]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Merge branch 'release/3.3.0' into develop
authorChocobozzz <me@florianbigard.com>
Fri, 30 Jul 2021 09:38:19 +0000 (11:38 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 30 Jul 2021 09:38:19 +0000 (11:38 +0200)
590 files changed:
.github/workflows/test.yml
client/src/app/+about/about-instance/contact-admin-modal.component.ts
client/src/app/+accounts/accounts.component.ts
client/src/app/+admin/admin.component.ts
client/src/app/+admin/admin.module.ts
client/src/app/+admin/follows/followers-list/followers-list.component.html
client/src/app/+admin/follows/following-list/follow-modal.component.html [new file with mode: 0644]
client/src/app/+admin/follows/following-list/follow-modal.component.scss [new file with mode: 0644]
client/src/app/+admin/follows/following-list/follow-modal.component.ts [new file with mode: 0644]
client/src/app/+admin/follows/following-list/following-list.component.html
client/src/app/+admin/follows/following-list/following-list.component.ts
client/src/app/+admin/follows/following-list/index.ts
client/src/app/+admin/follows/follows.routes.ts
client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
client/src/app/+admin/users/user-list/user-list.component.ts
client/src/app/+login/login.component.html
client/src/app/+login/login.component.ts
client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts
client/src/app/+my-library/my-videos/my-videos.component.html
client/src/app/+page-not-found/page-not-found.component.ts
client/src/app/+search/search-filters.component.html
client/src/app/+search/search-filters.component.ts
client/src/app/+search/search.component.html
client/src/app/+search/search.component.scss
client/src/app/+search/search.component.ts
client/src/app/+video-channels/video-channels.component.ts
client/src/app/+videos/+video-edit/shared/video-edit.component.html
client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts
client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html
client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss
client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.ts
client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts
client/src/app/+videos/+video-watch/video-watch.component.ts
client/src/app/core/auth/auth.service.ts
client/src/app/core/menu/menu.service.ts
client/src/app/core/renderer/markdown.service.ts
client/src/app/core/rest/rest-extractor.service.ts
client/src/app/helpers/utils.ts
client/src/app/shared/form-validators/batch-domains-validators.ts [deleted file]
client/src/app/shared/form-validators/host-validators.ts [new file with mode: 0644]
client/src/app/shared/form-validators/host.ts [deleted file]
client/src/app/shared/form-validators/index.ts
client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
client/src/app/shared/shared-custom-markup/peertube-custom-tags/embed-markup.component.ts
client/src/app/shared/shared-forms/markdown-textarea.component.ts
client/src/app/shared/shared-forms/timestamp-input.component.ts
client/src/app/shared/shared-instance/instance-follow.service.ts
client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
client/src/app/shared/shared-main/users/user-notification.model.ts
client/src/app/shared/shared-main/users/user-notifications.component.ts
client/src/app/shared/shared-main/video/video.model.ts
client/src/app/shared/shared-moderation/batch-domains-modal.component.html
client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
client/src/app/shared/shared-search/advanced-search.model.ts
client/src/app/shared/shared-search/search.service.ts
client/src/app/shared/shared-share-modal/video-share.component.ts
client/src/app/shared/shared-video-miniature/abstract-video-list.ts
client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
client/src/app/shared/shared-video-playlist/video-playlist.model.ts
client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
client/src/assets/player/peertube-player-manager.ts
client/src/assets/player/peertube-plugin.ts
client/src/assets/player/peertube-videojs-typings.ts
client/src/assets/player/playlist/playlist-menu-item.ts
client/src/assets/player/stats/stats-card.ts
client/src/assets/player/utils.ts
client/src/assets/player/videojs-components/peertube-link-button.ts
client/src/assets/player/webtorrent/webtorrent-plugin.ts
client/src/locale/angular.ar.xlf
client/src/locale/angular.ca-ES.xlf
client/src/locale/angular.cs-CZ.xlf
client/src/locale/angular.da-DK.xlf
client/src/locale/angular.de-DE.xlf
client/src/locale/angular.el-GR.xlf
client/src/locale/angular.en-GB.xlf
client/src/locale/angular.en-US.xlf
client/src/locale/angular.eo.xlf
client/src/locale/angular.es-ES.xlf
client/src/locale/angular.eu-ES.xlf
client/src/locale/angular.fa-IR.xlf
client/src/locale/angular.fi-FI.xlf
client/src/locale/angular.fr-FR.xlf
client/src/locale/angular.gd.xlf
client/src/locale/angular.gl-ES.xlf
client/src/locale/angular.hu-HU.xlf
client/src/locale/angular.it-IT.xlf
client/src/locale/angular.ja-JP.xlf
client/src/locale/angular.jbo.xlf
client/src/locale/angular.kab.xlf
client/src/locale/angular.ko-KR.xlf
client/src/locale/angular.lt-LT.xlf
client/src/locale/angular.nb-NO.xlf
client/src/locale/angular.nl-NL.xlf
client/src/locale/angular.oc.xlf
client/src/locale/angular.pl-PL.xlf
client/src/locale/angular.pt-BR.xlf
client/src/locale/angular.pt-PT.xlf
client/src/locale/angular.ru-RU.xlf
client/src/locale/angular.sk-SK.xlf
client/src/locale/angular.sl-SI.xlf
client/src/locale/angular.sv-SE.xlf
client/src/locale/angular.ta.xlf
client/src/locale/angular.th-TH.xlf
client/src/locale/angular.tr-TR.xlf
client/src/locale/angular.uk-UA.xlf
client/src/locale/angular.vi-VN.xlf
client/src/locale/angular.xlf
client/src/locale/angular.zh-Hans-CN.xlf
client/src/locale/angular.zh-Hant-TW.xlf
client/src/sass/application.scss
client/src/standalone/videos/embed.ts
package.json
scripts/benchmark.ts
scripts/ci.sh
scripts/generate-code-contributors.ts
scripts/optimize-old-videos.ts
scripts/parse-log.ts
scripts/prune-storage.ts
scripts/update-host.ts
server.ts
server/controllers/activitypub/client.ts
server/controllers/activitypub/inbox.ts
server/controllers/api/abuse.ts
server/controllers/api/accounts.ts
server/controllers/api/bulk.ts
server/controllers/api/config.ts
server/controllers/api/custom-page.ts
server/controllers/api/index.ts
server/controllers/api/oauth-clients.ts
server/controllers/api/overviews.ts
server/controllers/api/plugins.ts
server/controllers/api/search/search-video-channels.ts
server/controllers/api/search/search-video-playlists.ts
server/controllers/api/search/search-videos.ts
server/controllers/api/server/contact.ts
server/controllers/api/server/debug.ts
server/controllers/api/server/follows.ts
server/controllers/api/server/index.ts
server/controllers/api/server/logs.ts
server/controllers/api/server/redundancy.ts
server/controllers/api/server/server-blocklist.ts
server/controllers/api/server/stats.ts
server/controllers/api/users/index.ts
server/controllers/api/users/me.ts
server/controllers/api/users/my-blocklist.ts
server/controllers/api/users/my-history.ts
server/controllers/api/users/my-notifications.ts
server/controllers/api/users/my-subscriptions.ts
server/controllers/api/users/my-video-playlists.ts
server/controllers/api/video-channel.ts
server/controllers/api/video-playlist.ts
server/controllers/api/videos/blacklist.ts
server/controllers/api/videos/captions.ts
server/controllers/api/videos/comment.ts
server/controllers/api/videos/index.ts
server/controllers/api/videos/live.ts
server/controllers/api/videos/ownership.ts
server/controllers/api/videos/rate.ts
server/controllers/api/videos/update.ts
server/controllers/api/videos/upload.ts
server/controllers/api/videos/watching.ts
server/controllers/bots.ts
server/controllers/client.ts
server/controllers/download.ts
server/controllers/feeds.ts
server/controllers/lazy-static.ts
server/controllers/live.ts
server/controllers/plugins.ts
server/controllers/static.ts
server/helpers/custom-validators/follows.ts
server/helpers/custom-validators/misc.ts
server/helpers/custom-validators/servers.ts
server/helpers/custom-validators/video-ownership.ts
server/helpers/database-utils.ts
server/helpers/express-utils.ts
server/helpers/ffmpeg-utils.ts
server/helpers/logger.ts
server/helpers/query.ts [new file with mode: 0644]
server/helpers/webtorrent.ts
server/helpers/youtube-dl.ts
server/initializers/constants.ts
server/initializers/migrations/0655-streaming-playlist-filenames.ts [new file with mode: 0644]
server/lib/activitypub/actors/refresh.ts
server/lib/activitypub/crawl.ts
server/lib/activitypub/follow.ts
server/lib/activitypub/playlists/refresh.ts
server/lib/activitypub/videos/refresh.ts
server/lib/activitypub/videos/shared/abstract-builder.ts
server/lib/activitypub/videos/shared/object-to-model-attributes.ts
server/lib/client-html.ts
server/lib/hls.ts
server/lib/job-queue/handlers/activitypub-cleaner.ts
server/lib/job-queue/handlers/video-file-import.ts
server/lib/job-queue/handlers/video-import.ts
server/lib/job-queue/handlers/video-live-ending.ts
server/lib/job-queue/handlers/video-transcoding.ts
server/lib/live/live-manager.ts
server/lib/live/shared/muxing-session.ts
server/lib/moderation.ts
server/lib/plugins/register-helpers.ts
server/lib/plugins/video-constant-manager-factory.ts [new file with mode: 0644]
server/lib/schedulers/plugins-check-scheduler.ts
server/lib/schedulers/videos-redundancy-scheduler.ts
server/lib/transcoding/video-transcoding.ts
server/lib/video-paths.ts
server/lib/video.ts
server/middlewares/activitypub.ts
server/middlewares/auth.ts
server/middlewares/cache.ts [deleted file]
server/middlewares/cache/cache.ts [new file with mode: 0644]
server/middlewares/cache/index.ts [new file with mode: 0644]
server/middlewares/cache/shared/api-cache.ts [new file with mode: 0644]
server/middlewares/cache/shared/index.ts [new file with mode: 0644]
server/middlewares/error.ts
server/middlewares/index.ts
server/middlewares/servers.ts
server/middlewares/user-right.ts
server/middlewares/validators/abuse.ts
server/middlewares/validators/activitypub/activity.ts
server/middlewares/validators/blocklist.ts
server/middlewares/validators/bulk.ts
server/middlewares/validators/feeds.ts
server/middlewares/validators/follows.ts
server/middlewares/validators/oembed.ts
server/middlewares/validators/plugins.ts
server/middlewares/validators/redundancy.ts
server/middlewares/validators/search.ts
server/middlewares/validators/server.ts
server/middlewares/validators/shared/abuses.ts
server/middlewares/validators/shared/accounts.ts
server/middlewares/validators/shared/video-blacklists.ts
server/middlewares/validators/shared/video-captions.ts
server/middlewares/validators/shared/video-channels.ts
server/middlewares/validators/shared/video-comments.ts
server/middlewares/validators/shared/video-imports.ts
server/middlewares/validators/shared/video-ownerships.ts
server/middlewares/validators/shared/video-playlists.ts
server/middlewares/validators/shared/videos.ts
server/middlewares/validators/themes.ts
server/middlewares/validators/user-subscriptions.ts
server/middlewares/validators/users.ts
server/middlewares/validators/videos/video-blacklist.ts
server/middlewares/validators/videos/video-channels.ts
server/middlewares/validators/videos/video-comments.ts
server/middlewares/validators/videos/video-imports.ts
server/middlewares/validators/videos/video-live.ts
server/middlewares/validators/videos/video-ownership-changes.ts
server/middlewares/validators/videos/video-playlists.ts
server/middlewares/validators/videos/video-rates.ts
server/middlewares/validators/videos/video-shares.ts
server/middlewares/validators/videos/video-watch.ts
server/middlewares/validators/videos/videos.ts
server/middlewares/validators/webfinger.ts
server/models/account/account.ts
server/models/actor/actor-follow.ts
server/models/redundancy/video-redundancy.ts
server/models/shared/index.ts [new file with mode: 0644]
server/models/shared/query.ts [new file with mode: 0644]
server/models/shared/update.ts [new file with mode: 0644]
server/models/user/user-notification.ts
server/models/video/formatter/video-format-utils.ts
server/models/video/sql/shared/video-tables.ts
server/models/video/sql/videos-id-list-query-builder.ts
server/models/video/video-channel.ts
server/models/video/video-file.ts
server/models/video/video-playlist.ts
server/models/video/video-streaming-playlist.ts
server/models/video/video.ts
server/tests/api/activitypub/cleaner.ts
server/tests/api/activitypub/client.ts
server/tests/api/activitypub/fetch.ts
server/tests/api/activitypub/helpers.ts
server/tests/api/activitypub/refresher.ts
server/tests/api/activitypub/security.ts
server/tests/api/check-params/abuses.ts
server/tests/api/check-params/accounts.ts
server/tests/api/check-params/blocklist.ts
server/tests/api/check-params/bulk.ts
server/tests/api/check-params/config.ts
server/tests/api/check-params/contact-form.ts
server/tests/api/check-params/custom-pages.ts
server/tests/api/check-params/debug.ts
server/tests/api/check-params/follows.ts
server/tests/api/check-params/jobs.ts
server/tests/api/check-params/live.ts
server/tests/api/check-params/logs.ts
server/tests/api/check-params/plugins.ts
server/tests/api/check-params/redundancy.ts
server/tests/api/check-params/search.ts
server/tests/api/check-params/services.ts
server/tests/api/check-params/upload-quota.ts
server/tests/api/check-params/user-notifications.ts
server/tests/api/check-params/user-subscriptions.ts
server/tests/api/check-params/users.ts
server/tests/api/check-params/video-blacklist.ts
server/tests/api/check-params/video-captions.ts
server/tests/api/check-params/video-channels.ts
server/tests/api/check-params/video-comments.ts
server/tests/api/check-params/video-imports.ts
server/tests/api/check-params/video-playlists.ts
server/tests/api/check-params/videos-filter.ts
server/tests/api/check-params/videos-history.ts
server/tests/api/check-params/videos-overviews.ts
server/tests/api/check-params/videos.ts
server/tests/api/live/live-constraints.ts
server/tests/api/live/live-permanent.ts
server/tests/api/live/live-save-replay.ts
server/tests/api/live/live-socket-messages.ts
server/tests/api/live/live-views.ts
server/tests/api/live/live.ts
server/tests/api/moderation/abuses.ts
server/tests/api/moderation/blocklist-notification.ts
server/tests/api/moderation/blocklist.ts
server/tests/api/moderation/video-blacklist.ts
server/tests/api/notifications/admin-notifications.ts
server/tests/api/notifications/comments-notifications.ts
server/tests/api/notifications/moderation-notifications.ts
server/tests/api/notifications/notifications-api.ts
server/tests/api/notifications/user-notifications.ts
server/tests/api/redundancy/manage-redundancy.ts
server/tests/api/redundancy/redundancy-constraints.ts
server/tests/api/redundancy/redundancy.ts
server/tests/api/search/search-activitypub-video-channels.ts
server/tests/api/search/search-activitypub-video-playlists.ts
server/tests/api/search/search-activitypub-videos.ts
server/tests/api/search/search-channels.ts
server/tests/api/search/search-index.ts
server/tests/api/search/search-playlists.ts
server/tests/api/search/search-videos.ts
server/tests/api/server/auto-follows.ts
server/tests/api/server/bulk.ts
server/tests/api/server/config.ts
server/tests/api/server/contact-form.ts
server/tests/api/server/email.ts
server/tests/api/server/follow-constraints.ts
server/tests/api/server/follows-moderation.ts
server/tests/api/server/follows.ts
server/tests/api/server/handle-down.ts
server/tests/api/server/homepage.ts
server/tests/api/server/jobs.ts
server/tests/api/server/logs.ts
server/tests/api/server/no-client.ts
server/tests/api/server/plugins.ts
server/tests/api/server/reverse-proxy.ts
server/tests/api/server/services.ts
server/tests/api/server/stats.ts
server/tests/api/server/tracker.ts
server/tests/api/users/user-subscriptions.ts
server/tests/api/users/users-multiple-servers.ts
server/tests/api/users/users-verification.ts
server/tests/api/users/users.ts
server/tests/api/videos/audio-only.ts
server/tests/api/videos/multiple-servers.ts
server/tests/api/videos/resumable-upload.ts
server/tests/api/videos/single-server.ts
server/tests/api/videos/video-captions.ts
server/tests/api/videos/video-change-ownership.ts
server/tests/api/videos/video-channels.ts
server/tests/api/videos/video-comments.ts
server/tests/api/videos/video-description.ts
server/tests/api/videos/video-hls.ts
server/tests/api/videos/video-imports.ts
server/tests/api/videos/video-nsfw.ts
server/tests/api/videos/video-playlist-thumbnails.ts
server/tests/api/videos/video-playlists.ts
server/tests/api/videos/video-privacy.ts
server/tests/api/videos/video-schedule-update.ts
server/tests/api/videos/video-transcoder.ts
server/tests/api/videos/videos-filter.ts
server/tests/api/videos/videos-history.ts
server/tests/api/videos/videos-overview.ts
server/tests/api/videos/videos-views-cleaner.ts
server/tests/cli/create-import-video-file-job.ts
server/tests/cli/create-transcoding-job.ts
server/tests/cli/optimize-old-videos.ts
server/tests/cli/peertube.ts
server/tests/cli/plugins.ts
server/tests/cli/print-transcode-command.ts
server/tests/cli/prune-storage.ts
server/tests/cli/regenerate-thumbnails.ts
server/tests/cli/reset-password.ts
server/tests/cli/update-host.ts
server/tests/client.ts
server/tests/external-plugins/auth-ldap.ts
server/tests/external-plugins/auto-block-videos.ts
server/tests/external-plugins/auto-mute.ts
server/tests/feeds/feeds.ts
server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js
server/tests/fixtures/peertube-plugin-test-video-constants/main.js
server/tests/fixtures/peertube-plugin-test/main.js
server/tests/fixtures/video_very_short_240p.mp4 [new file with mode: 0644]
server/tests/helpers/comment-model.ts
server/tests/helpers/core-utils.ts
server/tests/helpers/image.ts
server/tests/helpers/request.ts
server/tests/index.ts
server/tests/lib/index.ts [new file with mode: 0644]
server/tests/lib/video-constant-registry-factory.ts [new file with mode: 0644]
server/tests/misc-endpoints.ts
server/tests/plugins/action-hooks.ts
server/tests/plugins/external-auth.ts
server/tests/plugins/filter-hooks.ts
server/tests/plugins/html-injection.ts
server/tests/plugins/id-and-pass-auth.ts
server/tests/plugins/plugin-helpers.ts
server/tests/plugins/plugin-router.ts
server/tests/plugins/plugin-storage.ts
server/tests/plugins/plugin-transcoding.ts
server/tests/plugins/plugin-unloading.ts
server/tests/plugins/translations.ts
server/tests/plugins/video-constants.ts
server/tools/cli.ts
server/tools/peertube-auth.ts
server/tools/peertube-get-access-token.ts
server/tools/peertube-import-videos.ts
server/tools/peertube-plugins.ts
server/tools/peertube-redundancy.ts
server/tools/peertube-upload.ts
server/tools/test-live.ts [moved from server/tools/test.ts with 59% similarity]
server/types/models/video/video-streaming-playlist.ts
server/typings/express/index.d.ts
shared/core-utils/common/date.ts [moved from shared/core-utils/miscs/date.ts with 53% similarity]
shared/core-utils/common/index.ts [new file with mode: 0644]
shared/core-utils/common/miscs.ts [moved from shared/core-utils/miscs/miscs.ts with 81% similarity]
shared/core-utils/common/promises.ts [new file with mode: 0644]
shared/core-utils/common/regexp.ts [new file with mode: 0644]
shared/core-utils/common/types.ts [moved from shared/core-utils/miscs/types.ts with 100% similarity]
shared/core-utils/common/url.ts [new file with mode: 0644]
shared/core-utils/index.ts
shared/core-utils/logs/index.ts [deleted file]
shared/core-utils/logs/logs.ts [deleted file]
shared/core-utils/miscs/index.ts [deleted file]
shared/core-utils/plugins/hooks.ts
shared/core-utils/utils/index.ts [new file with mode: 0644]
shared/core-utils/utils/object.ts [new file with mode: 0644]
shared/extra-utils/bulk/bulk-command.ts [new file with mode: 0644]
shared/extra-utils/bulk/bulk.ts [deleted file]
shared/extra-utils/bulk/index.ts [new file with mode: 0644]
shared/extra-utils/cli/cli-command.ts [new file with mode: 0644]
shared/extra-utils/cli/cli.ts [deleted file]
shared/extra-utils/cli/index.ts [new file with mode: 0644]
shared/extra-utils/custom-pages/custom-pages-command.ts [new file with mode: 0644]
shared/extra-utils/custom-pages/custom-pages.ts [deleted file]
shared/extra-utils/custom-pages/index.ts [new file with mode: 0644]
shared/extra-utils/feeds/feeds-command.ts [new file with mode: 0644]
shared/extra-utils/feeds/feeds.ts [deleted file]
shared/extra-utils/feeds/index.ts [new file with mode: 0644]
shared/extra-utils/index.ts
shared/extra-utils/logs/index.ts [new file with mode: 0644]
shared/extra-utils/logs/logs-command.ts [new file with mode: 0644]
shared/extra-utils/logs/logs.ts [deleted file]
shared/extra-utils/miscs/checks.ts [new file with mode: 0644]
shared/extra-utils/miscs/generate.ts [new file with mode: 0644]
shared/extra-utils/miscs/index.ts [new file with mode: 0644]
shared/extra-utils/miscs/miscs.ts [deleted file]
shared/extra-utils/miscs/sql-command.ts [new file with mode: 0644]
shared/extra-utils/miscs/sql.ts [deleted file]
shared/extra-utils/miscs/stubs.ts [deleted file]
shared/extra-utils/miscs/tests.ts [new file with mode: 0644]
shared/extra-utils/miscs/webtorrent.ts [new file with mode: 0644]
shared/extra-utils/mock-servers/index.ts [new file with mode: 0644]
shared/extra-utils/mock-servers/mock-email.ts [moved from shared/extra-utils/miscs/email.ts with 92% similarity]
shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts [moved from shared/extra-utils/mock-servers/joinpeertube-versions.ts with 100% similarity]
shared/extra-utils/mock-servers/mock-plugin-blocklist.ts [moved from shared/extra-utils/plugins/mock-blocklist.ts with 100% similarity]
shared/extra-utils/moderation/abuses-command.ts [new file with mode: 0644]
shared/extra-utils/moderation/abuses.ts [deleted file]
shared/extra-utils/moderation/index.ts [new file with mode: 0644]
shared/extra-utils/overviews/index.ts [new file with mode: 0644]
shared/extra-utils/overviews/overviews-command.ts [new file with mode: 0644]
shared/extra-utils/overviews/overviews.ts [deleted file]
shared/extra-utils/requests/activitypub.ts
shared/extra-utils/requests/check-api-params.ts
shared/extra-utils/requests/index.ts [new file with mode: 0644]
shared/extra-utils/requests/requests.ts
shared/extra-utils/search/index.ts [new file with mode: 0644]
shared/extra-utils/search/search-command.ts [new file with mode: 0644]
shared/extra-utils/search/video-channels.ts [deleted file]
shared/extra-utils/search/video-playlists.ts [deleted file]
shared/extra-utils/search/videos.ts [deleted file]
shared/extra-utils/server/activitypub.ts [deleted file]
shared/extra-utils/server/clients.ts [deleted file]
shared/extra-utils/server/config-command.ts [new file with mode: 0644]
shared/extra-utils/server/config.ts [deleted file]
shared/extra-utils/server/contact-form-command.ts [new file with mode: 0644]
shared/extra-utils/server/contact-form.ts [deleted file]
shared/extra-utils/server/debug-command.ts [new file with mode: 0644]
shared/extra-utils/server/debug.ts [deleted file]
shared/extra-utils/server/directories.ts [new file with mode: 0644]
shared/extra-utils/server/follows-command.ts [new file with mode: 0644]
shared/extra-utils/server/follows.ts
shared/extra-utils/server/index.ts [new file with mode: 0644]
shared/extra-utils/server/jobs-command.ts [new file with mode: 0644]
shared/extra-utils/server/jobs.ts
shared/extra-utils/server/plugins-command.ts [new file with mode: 0644]
shared/extra-utils/server/plugins.ts
shared/extra-utils/server/redundancy-command.ts [new file with mode: 0644]
shared/extra-utils/server/redundancy.ts [deleted file]
shared/extra-utils/server/server.ts [new file with mode: 0644]
shared/extra-utils/server/servers-command.ts [new file with mode: 0644]
shared/extra-utils/server/servers.ts
shared/extra-utils/server/stats-command.ts [new file with mode: 0644]
shared/extra-utils/server/stats.ts [deleted file]
shared/extra-utils/shared/abstract-command.ts [new file with mode: 0644]
shared/extra-utils/shared/index.ts [new file with mode: 0644]
shared/extra-utils/socket/index.ts [new file with mode: 0644]
shared/extra-utils/socket/socket-io-command.ts [new file with mode: 0644]
shared/extra-utils/socket/socket-io.ts [deleted file]
shared/extra-utils/users/accounts-command.ts [new file with mode: 0644]
shared/extra-utils/users/accounts.ts [deleted file]
shared/extra-utils/users/actors.ts [new file with mode: 0644]
shared/extra-utils/users/blocklist-command.ts [new file with mode: 0644]
shared/extra-utils/users/blocklist.ts [deleted file]
shared/extra-utils/users/index.ts [new file with mode: 0644]
shared/extra-utils/users/login-command.ts [new file with mode: 0644]
shared/extra-utils/users/login.ts
shared/extra-utils/users/notifications-command.ts [new file with mode: 0644]
shared/extra-utils/users/notifications.ts [moved from shared/extra-utils/users/user-notifications.ts with 63% similarity]
shared/extra-utils/users/subscriptions-command.ts [new file with mode: 0644]
shared/extra-utils/users/user-subscriptions.ts [deleted file]
shared/extra-utils/users/users-command.ts [new file with mode: 0644]
shared/extra-utils/users/users.ts [deleted file]
shared/extra-utils/videos/blacklist-command.ts [new file with mode: 0644]
shared/extra-utils/videos/captions-command.ts [new file with mode: 0644]
shared/extra-utils/videos/captions.ts [new file with mode: 0644]
shared/extra-utils/videos/change-ownership-command.ts [new file with mode: 0644]
shared/extra-utils/videos/channels-command.ts [new file with mode: 0644]
shared/extra-utils/videos/channels.ts [new file with mode: 0644]
shared/extra-utils/videos/comments-command.ts [new file with mode: 0644]
shared/extra-utils/videos/history-command.ts [new file with mode: 0644]
shared/extra-utils/videos/imports-command.ts [new file with mode: 0644]
shared/extra-utils/videos/index.ts [new file with mode: 0644]
shared/extra-utils/videos/live-command.ts [new file with mode: 0644]
shared/extra-utils/videos/live.ts
shared/extra-utils/videos/playlists-command.ts [new file with mode: 0644]
shared/extra-utils/videos/playlists.ts [new file with mode: 0644]
shared/extra-utils/videos/services-command.ts [new file with mode: 0644]
shared/extra-utils/videos/services.ts [deleted file]
shared/extra-utils/videos/streaming-playlists-command.ts [new file with mode: 0644]
shared/extra-utils/videos/streaming-playlists.ts [new file with mode: 0644]
shared/extra-utils/videos/video-blacklist.ts [deleted file]
shared/extra-utils/videos/video-captions.ts [deleted file]
shared/extra-utils/videos/video-change-ownership.ts [deleted file]
shared/extra-utils/videos/video-channels.ts [deleted file]
shared/extra-utils/videos/video-comments.ts [deleted file]
shared/extra-utils/videos/video-history.ts [deleted file]
shared/extra-utils/videos/video-imports.ts [deleted file]
shared/extra-utils/videos/video-playlists.ts [deleted file]
shared/extra-utils/videos/video-streaming-playlists.ts [deleted file]
shared/extra-utils/videos/videos-command.ts [new file with mode: 0644]
shared/extra-utils/videos/videos.ts
shared/models/http/http-error-codes.ts [moved from shared/core-utils/miscs/http-error-codes.ts with 100% similarity]
shared/models/http/http-methods.ts [moved from shared/core-utils/miscs/http-methods.ts with 100% similarity]
shared/models/http/index.ts [new file with mode: 0644]
shared/models/index.ts
shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts
shared/models/plugins/server/managers/plugin-video-category-manager.model.ts
shared/models/plugins/server/managers/plugin-video-language-manager.model.ts
shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts
shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts
shared/models/plugins/server/plugin-constant-manager.model.ts [new file with mode: 0644]
shared/models/plugins/server/server-hook.model.ts
shared/models/search/video-channels-search-query.model.ts
shared/models/search/video-playlists-search-query.model.ts
shared/models/search/videos-common-query.model.ts
shared/models/search/videos-search-query.model.ts
shared/models/server/debug.model.ts
shared/models/server/index.ts
shared/models/server/peertube-problem-document.model.ts
shared/models/server/server-follow-create.model.ts [new file with mode: 0644]
shared/models/users/index.ts
shared/models/users/user-create-result.model.ts [new file with mode: 0644]
shared/models/users/user-notification.model.ts
shared/models/videos/channel/index.ts
shared/models/videos/channel/video-channel-create-result.model.ts [new file with mode: 0644]
shared/models/videos/comment/index.ts
shared/models/videos/comment/video-comment-create.model.ts [new file with mode: 0644]
shared/models/videos/comment/video-comment.model.ts
shared/models/videos/playlist/index.ts
shared/models/videos/playlist/video-playlist-element-create-result.model.ts [new file with mode: 0644]
shared/models/videos/video-update.model.ts
support/doc/api/openapi.yaml
support/doc/dependencies.md
support/doc/plugins/guide.md
tsconfig.json
yarn.lock

index 46b243244b9c02a118a059c5f4d035c2e6e449d6..c5bbd9e2c64d439a250b27e5e25227f811bdfc94 100644 (file)
@@ -2,11 +2,6 @@ name: Test Suite
 
 on:
   push:
-    branches:
-      - develop
-      - master
-      - ci
-      - next
   pull_request:
     types: [synchronize, opened]
   schedule:
index a528faa20448839914ebb17e8fe3c1d21b9978d0..37e9feacb0951896af18f884b2e690aeea92626d 100644 (file)
@@ -11,8 +11,7 @@ import { FormReactive, FormValidatorService } 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 { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { HTMLServerConfig } from '@shared/models'
+import { HTMLServerConfig, HttpStatusCode } from '@shared/models'
 
 type Prefill = {
   subject?: string
index c69b04a01bd301ce27419e43e6aded4c12535bb0..5b59f3cd0e556ea73d73a82a456238e999c7bfd9 100644 (file)
@@ -13,8 +13,7 @@ import {
   VideoService
 } from '@app/shared/shared-main'
 import { AccountReportComponent } from '@app/shared/shared-moderation'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { User, UserRight } from '@shared/models'
+import { HttpStatusCode, User, UserRight } from '@shared/models'
 import { AccountSearchComponent } from './account-search/account-search.component'
 
 @Component({
index dd92ed2caffaed20564d9d331ab116e800ac3d21..4b6fab6ed382e2b47350c61346febb5fcf00d77b 100644 (file)
@@ -26,12 +26,12 @@ export class AdminComponent implements OnInit {
       label: $localize`Federation`,
       children: [
         {
-          label: $localize`Instances you follow`,
+          label: $localize`Following`,
           routerLink: '/admin/follows/following-list',
           iconName: 'following'
         },
         {
-          label: $localize`Instances following you`,
+          label: $localize`Followers`,
           routerLink: '/admin/follows/followers-list',
           iconName: 'follower'
         },
index a7fe20b074cb12c015ab843ac10cbed465bce987..1ea7b9784ab08f4b2c9537dc2b71780a7e0ed14d 100644 (file)
@@ -25,7 +25,7 @@ import {
   EditVODTranscodingComponent
 } from './config'
 import { ConfigService } from './config/shared/config.service'
-import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
+import { FollowersListComponent, FollowModalComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
 import { FollowingListComponent } from './follows/following-list/following-list.component'
 import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
 import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
@@ -68,6 +68,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
     FollowsComponent,
     FollowersListComponent,
     FollowingListComponent,
+    FollowModalComponent,
     RedundancyCheckboxComponent,
     VideoRedundanciesListComponent,
     VideoRedundancyInformationComponent,
index c2e9a4df69fa61d6537697a2ccc17a8def72fd09..08459634d76a1701d7d6ccbd0e805e33a4cd1929 100644 (file)
@@ -1,6 +1,6 @@
 <h1>
   <my-global-icon iconName="follower" aria-hidden="true"></my-global-icon>
-  <ng-container i18n>Instances following you</ng-container>
+  <ng-container i18n>Followers of your instance</ng-container>
 </h1>
 
 <p-table
@@ -21,7 +21,7 @@
   <ng-template pTemplate="header">
     <tr>
       <th style="width: 150px;" i18n>Actions</th>
-      <th i18n>Follower handle</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>
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.html b/client/src/app/+admin/follows/following-list/follow-modal.component.html
new file mode 100644 (file)
index 0000000..d0761b7
--- /dev/null
@@ -0,0 +1,42 @@
+<ng-template #modal>
+  <div class="modal-header">
+    <h4 i18n class="modal-title">Follow</h4>
+
+    <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+  </div>
+
+  <div class="modal-body">
+    <form novalidate [formGroup]="form" (ngSubmit)="submit()">
+      <div class="form-group">
+        <label i18n for="hostsOrHandles">1 host (without "http://"), account handle or channel handle per line</label>
+
+        <textarea
+          [placeholder]="placeholder" formControlName="hostsOrHandles" type="text" id="hostsOrHandles" name="hostsOrHandles"
+          class="form-control" [ngClass]="{ 'input-error': formErrors['hostsOrHandles'] }" ngbAutofocus
+        ></textarea>
+
+        <div *ngIf="formErrors.hostsOrHandles" class="form-error">
+          {{ formErrors.hostsOrHandles }}
+
+          <div *ngIf="form.controls['hostsOrHandles'].errors.validHostsOrHandles">
+            {{ form.controls['hostsOrHandles'].errors.validHostsOrHandles.value }}
+          </div>
+        </div>
+      </div>
+
+      <div i18n *ngIf="httpEnabled() === false"  class="alert alert-warning">
+        It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
+      </div>
+
+      <div class="form-group inputs">
+        <input
+          type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
+          (click)="hide()" (key.enter)="hide()"
+        >
+
+        <input type="submit" i18n-value value="Follow" class="peertube-button orange-button" [disabled]="!form.valid" />
+      </div>
+    </form>
+  </div>
+
+</ng-template>
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.scss b/client/src/app/+admin/follows/following-list/follow-modal.component.scss
new file mode 100644 (file)
index 0000000..9621a56
--- /dev/null
@@ -0,0 +1,3 @@
+textarea {
+  height: 200px;
+}
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.ts b/client/src/app/+admin/follows/following-list/follow-modal.component.ts
new file mode 100644 (file)
index 0000000..dc69092
--- /dev/null
@@ -0,0 +1,69 @@
+import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
+import { Notifier } from '@app/core'
+import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators'
+import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
+import { InstanceFollowService } from '@app/shared/shared-instance'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
+
+@Component({
+  selector: 'my-follow-modal',
+  templateUrl: './follow-modal.component.html',
+  styleUrls: [ './follow-modal.component.scss' ]
+})
+export class FollowModalComponent extends FormReactive implements OnInit {
+  @ViewChild('modal', { static: true }) modal: NgbModal
+
+  @Output() newFollow = new EventEmitter<void>()
+
+  placeholder = 'example.com\nchocobozzz@example.com\nchocobozzz_channel@example.com'
+
+  private openedModal: NgbModalRef
+
+  constructor (
+    protected formValidatorService: FormValidatorService,
+    private modalService: NgbModal,
+    private followService: InstanceFollowService,
+    private notifier: Notifier
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    this.buildForm({
+      hostsOrHandles: UNIQUE_HOSTS_OR_HANDLE_VALIDATOR
+    })
+  }
+
+  openModal () {
+    this.openedModal = this.modalService.open(this.modal, { centered: true })
+  }
+
+  hide () {
+    this.openedModal.close()
+  }
+
+  submit () {
+    this.addFollowing()
+
+    this.form.reset()
+    this.hide()
+  }
+
+  httpEnabled () {
+    return window.location.protocol === 'https:'
+  }
+
+  private async addFollowing () {
+    const hostsOrHandles = splitAndGetNotEmpty(this.form.value['hostsOrHandles'])
+
+    this.followService.follow(hostsOrHandles).subscribe(
+      () => {
+        this.notifier.success($localize`Follow request(s) sent!`)
+        this.newFollow.emit()
+      },
+
+      err => this.notifier.error(err.message)
+    )
+  }
+}
index e7c0c908823bc1ec4d6f9531a31ae7bf2e36d358..75b0efca85355f7de8f079275ebb4243f6bd4d5b 100644 (file)
@@ -1,6 +1,6 @@
 <h1>
   <my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
-  <ng-container i18n>Instances you follow</ng-container>
+  <ng-container i18n>Your instance subscriptions</ng-container>
 </h1>
 
 <p-table
@@ -13,9 +13,9 @@
   <ng-template pTemplate="caption">
     <div class="caption">
       <div class="left-buttons">
-        <a class="follow-button" (click)="addDomainsToFollow()" (key.enter)="addDomainsToFollow()">
+        <a class="follow-button" (click)="openFollowModal()" (key.enter)="openFollowModal()">
           <my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
-          <ng-container i18n>Follow instances</ng-container>
+          <ng-container i18n>Follow</ng-container>
         </a>
       </div>
 
@@ -28,7 +28,7 @@
   <ng-template pTemplate="header">
     <tr>
       <th style="width: 150px;" i18n>Action</th>
-      <th i18n>Host</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>
@@ -41,8 +41,8 @@
         <my-delete-button label="Unfollow" i18n-label (click)="removeFollowing(follow)"></my-delete-button>
       </td>
       <td>
-        <a [href]="'https://' + follow.following.host" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
-          {{ follow.following.host }}
+        <a [href]="follow.following.url" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
+          {{ follow.following.name + '@' + follow.following.host }}
           <span class="glyphicon glyphicon-new-window"></span>
         </a>
       </td>
@@ -57,6 +57,7 @@
       <td>{{ follow.createdAt | date: 'short' }}</td>
       <td>
         <my-redundancy-checkbox
+          *ngIf="isInstanceFollowing(follow)"
           [host]="follow.following.host" [redundancyAllowed]="follow.following.hostRedundancyAllowed"
         ></my-redundancy-checkbox>
       </td>
   </ng-template>
 </p-table>
 
-<my-batch-domains-modal #batchDomainsModal i18n-action action="Follow domains" (domains)="addFollowing($event)">
-  <ng-container ngProjectAs="warning">
-    <div i18n *ngIf="httpEnabled() === false"  class="alert alert-warning">
-      It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
-    </div>
-  </ng-container>
-</my-batch-domains-modal>
+<my-follow-modal #followModal></my-follow-modal>
index b63fe08c0b4b36f0854f9da306836f02338f8492..ba62dfa231a3d6a6fecfc027cf98e98d7c538b97 100644 (file)
@@ -4,13 +4,14 @@ import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
 import { InstanceFollowService } from '@app/shared/shared-instance'
 import { BatchDomainsModalComponent } from '@app/shared/shared-moderation'
 import { ActorFollow } from '@shared/models'
+import { FollowModalComponent } from './follow-modal.component'
 
 @Component({
   templateUrl: './following-list.component.html',
   styleUrls: [ '../follows.component.scss', './following-list.component.scss' ]
 })
 export class FollowingListComponent extends RestTable implements OnInit {
-  @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent
+  @ViewChild('followModal') followModal: FollowModalComponent
 
   following: ActorFollow[] = []
   totalRecords = 0
@@ -33,23 +34,12 @@ export class FollowingListComponent extends RestTable implements OnInit {
     return 'FollowingListComponent'
   }
 
-  addDomainsToFollow () {
-    this.batchDomainsModal.openModal()
+  openFollowModal () {
+    this.followModal.openModal()
   }
 
-  httpEnabled () {
-    return window.location.protocol === 'https:'
-  }
-
-  async addFollowing (hosts: string[]) {
-    this.followService.follow(hosts).subscribe(
-      () => {
-        this.notifier.success($localize`Follow request(s) sent!`)
-        this.reloadData()
-      },
-
-      err => this.notifier.error(err.message)
-    )
+  isInstanceFollowing (follow: ActorFollow) {
+    return follow.following.name === 'peertube'
   }
 
   async removeFollowing (follow: ActorFollow) {
index a70d46a7e6138f1c72285d93776fb9d1229be39c..88be0ed4cd8c0c73d571056eb520eab65365c1fd 100644 (file)
@@ -1 +1,2 @@
+export * from './follow-modal.component'
 export * from './following-list.component'
index cd70daf77dc439dfa740c45dd8561c3b9df6d124..3843b42b5e14a2d1a4c7372ab747f0b5d7424aaf 100644 (file)
@@ -25,7 +25,7 @@ export const FollowsRoutes: Routes = [
         component: FollowingListComponent,
         data: {
           meta: {
-            title: $localize`Following list`
+            title: $localize`Following`
           }
         }
       },
@@ -34,7 +34,7 @@ export const FollowsRoutes: Routes = [
         component: FollowersListComponent,
         data: {
           meta: {
-            title: $localize`Followers list`
+            title: $localize`Followers`
           }
         }
       },
index 08500ef5c10f8a68c37e7b0c5a6660ac3f0f03d6..4fe5ec441d0770c2098de60c5b4a8b3af5a1712d 100644 (file)
@@ -1,6 +1,6 @@
 import { SortMeta } from 'primeng/api'
 import { switchMap } from 'rxjs/operators'
-import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
+import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
 import { environment } from 'src/environments/environment'
 import { Component, OnInit } from '@angular/core'
 import { DomSanitizer } from '@angular/platform-browser'
@@ -9,6 +9,7 @@ import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, S
 import { AdvancedInputFilter } from '@app/shared/shared-forms'
 import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
 import { VideoBlockService } from '@app/shared/shared-moderation'
+import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils'
 import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
 
 @Component({
@@ -147,8 +148,9 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
 
   getVideoEmbed (entry: VideoBlacklist) {
     return buildVideoOrPlaylistEmbed(
-      buildVideoLink({
-        baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`,
+      decorateVideoLink({
+        url: buildVideoEmbedLink(entry.video, environment.originServerUrl),
+
         title: false,
         warningTitle: false
       }),
index 6af2249207b552e63b353dd30ca4dfd001fb8359..968abcbe5854c1d1119c37268789f32eb4675425 100644 (file)
@@ -4,7 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router'
 import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
 import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core'
 import { PluginService } from '@app/core/plugins/plugin.service'
-import { compareSemVer } from '@shared/core-utils/miscs/miscs'
+import { compareSemVer } from '@shared/core-utils'
 import { PeerTubePlugin, PluginType } from '@shared/models'
 
 @Component({
index e02d8e1ad8111e4cb36c61891bb83139367778a4..e3ae68a93bd26b2fa8b7a89de112c22a8650fbea 100644 (file)
@@ -108,18 +108,18 @@ export class UserListComponent extends RestTable implements OnInit {
     ]
 
     this.columns = [
-      { id: 'username', label: 'Username' },
-      { id: 'email', label: 'Email' },
-      { id: 'quota', label: 'Video quota' },
-      { id: 'role', label: 'Role' },
-      { id: 'createdAt', label: 'Created' }
+      { id: 'username', label: $localize`Username` },
+      { id: 'email', label: $localize`Email` },
+      { id: 'quota', label: $localize`Video quota` },
+      { id: 'role', label: $localize`Role` },
+      { id: 'createdAt', label: $localize`Created` }
     ]
 
     this.selectedColumns = this.columns.map(c => c.id)
 
-    this.columns.push({ id: 'quotaDaily', label: 'Daily quota' })
-    this.columns.push({ id: 'pluginAuth', label: 'Auth plugin' })
-    this.columns.push({ id: 'lastLoginDate', label: 'Last login' })
+    this.columns.push({ id: 'quotaDaily', label: $localize`Daily quota` })
+    this.columns.push({ id: 'pluginAuth', label: $localize`Auth plugin` })
+    this.columns.push({ id: 'lastLoginDate', label: $localize`Last login` })
   }
 
   getIdentifier () {
index 5f5b0f565235ab3e8a5f518140365d4dbf1a2fa1..27793ff0cc9345447614f5f8914aff1bd2e440ac 100644 (file)
             <div *ngIf="formErrors.username" class="form-error">
               {{ formErrors.username }}
             </div>
+
+            <div *ngIf="hasUsernameUppercase()" i18n class="form-warning">
+              ⚠️ Most email addresses do not include capital letters.
+            </div>
           </div>
 
           <div class="form-group">
index d8ad49081caec79b8fc99db4648a4ab1611b695b..9731383afc78cb1a1b0c61e78689389368d49bfa 100644 (file)
@@ -141,6 +141,10 @@ The link will expire within 1 hour.`
     this.accordion = instanceAboutAccordion.accordion
   }
 
+  hasUsernameUppercase () {
+    return this.form.value['username'].match(/[A-Z]/)
+  }
+
   private loadExternalAuthToken (username: string, token: string) {
     this.isAuthenticatedWithExternalAuth = true
 
index b3265210fdc0011953e0491cbea2f1510042082a..433475f666053946d2304b3210742716a299a340 100644 (file)
@@ -1,3 +1,5 @@
+import { of } from 'rxjs'
+import { switchMap } from 'rxjs/operators'
 import { Component, OnInit } from '@angular/core'
 import { Router } from '@angular/router'
 import { AuthService, Notifier } from '@app/core'
@@ -9,11 +11,8 @@ import {
 } from '@app/shared/form-validators/video-channel-validators'
 import { FormValidatorService } from '@app/shared/shared-forms'
 import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
-import { VideoChannelCreate } from '@shared/models'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode, VideoChannelCreate } from '@shared/models'
 import { MyVideoChannelEdit } from './my-video-channel-edit'
-import { switchMap } from 'rxjs/operators'
-import { of } from 'rxjs'
 
 @Component({
   templateUrl: './my-video-channel-edit.component.html',
index 67b3ee496a34d6b9164a053913c2628bc9a819f6..b6a2f592dcd954468b429a0a0e2d3c42e130a4be 100644 (file)
@@ -45,9 +45,9 @@ export class MyVideoChannelsComponent {
 It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another
 channel with the same name (${videoChannel.name})!`,
 
-      $localize`Please type the display name of the video channel (${videoChannel.displayName}) to confirm`,
+      $localize`Please type the name of the video channel (${videoChannel.name}) to confirm`,
 
-      videoChannel.displayName,
+      videoChannel.name,
 
       $localize`Delete`
     )
index 8d8b482add3756970736a71397b23fe0efb7b581..0552b8ce4fbde90f5f8690291795effdd6faefcc 100644 (file)
@@ -23,7 +23,7 @@
 
   <div class="peertube-select-container peertube-select-button">
     <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control">
-      <option value="undefined" disabled>Sort by</option>
+      <option value="undefined" disabled i18n>Sort by</option>
       <option value="-publishedAt" i18n>Last published first</option>
       <option value="-createdAt" i18n>Last created first</option>
       <option value="-views" i18n>Most viewed first</option>
index 639e5db78ccd6d12cde49acc631c5d7dcd1981a3..10645a634597a0269b358cac42f73d477cf98b43 100644 (file)
@@ -1,7 +1,8 @@
 import { Component, OnInit } from '@angular/core'
 import { Title } from '@angular/platform-browser'
 import { Router } from '@angular/router'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '@shared/models'
+
 @Component({
   selector: 'my-page-not-found',
   templateUrl: './page-not-found.component.html',
index 421bc7f6f1bd8a758e66bb892473d6be1c61fdf9..4b87a210209e199048820510aaea8ab92d87141c 100644 (file)
@@ -63,7 +63,7 @@
         </div>
 
         <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges">
-          <input type="radio" (change)="onInputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange">
+          <input type="radio" (change)="onDurationOrPublishedUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange">
           <label [for]="date.id" class="radio">{{ date.label }}</label>
         </div>
       </div>
@@ -79,7 +79,7 @@
         <div class="row">
           <div class="pl-0 col-sm-6">
             <input
-              (change)="onInputUpdated()"
+              (change)="onDurationOrPublishedUpdated()"
               (keydown.enter)="$event.preventDefault()"
               type="text" id="original-publication-after" name="original-publication-after"
               i18n-placeholder placeholder="After..."
@@ -89,7 +89,7 @@
           </div>
           <div class="pr-0 col-sm-6">
             <input
-              (change)="onInputUpdated()"
+              (change)="onDurationOrPublishedUpdated()"
               (keydown.enter)="$event.preventDefault()"
               type="text" id="original-publication-before" name="original-publication-before"
               i18n-placeholder placeholder="Before..."
         </div>
 
         <div class="peertube-radio-container" *ngFor="let duration of durationRanges">
-          <input type="radio" (change)="onInputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange">
+          <input type="radio" (change)="onDurationOrPublishedUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange">
           <label [for]="duration.id" class="radio">{{ duration.label }}</label>
         </div>
       </div>
         <my-select-tags name="tagsOneOf" labelForId="tagsOneOf" id="tagsOneOf" [(ngModel)]="advancedSearch.tagsOneOf"></my-select-tags>
       </div>
 
+      <div class="form-group">
+        <label i18n for="host">PeerTube instance host</label>
+
+        <input (change)="onDurationOrPublishedUpdated()" (keydown.enter)="$event.preventDefault()" type="text" id="host" name="host"
+          placeholder="example.com" [(ngModel)]="advancedSearch.host" class="form-control"
+        >
+      </div>
+
       <div class="form-group" *ngIf="isSearchTargetEnabled()">
         <div class="radio-label label-container">
           <label i18n>Search target</label>
index afa523b915ec30295c6081fcb5c7a15e67829d53..5972ba553201408f438d72b7a8f0cdb5b19e1137 100644 (file)
@@ -108,14 +108,14 @@ export class SearchFiltersComponent implements OnInit {
     this.loadOriginallyPublishedAtYears()
   }
 
-  onInputUpdated () {
+  onDurationOrPublishedUpdated () {
     this.updateModelFromDurationRange()
     this.updateModelFromPublishedRange()
     this.updateModelFromOriginallyPublishedAtYears()
   }
 
   formUpdated () {
-    this.onInputUpdated()
+    this.onDurationOrPublishedUpdated()
     this.filtered.emit(this.advancedSearch)
   }
 
@@ -127,7 +127,7 @@ export class SearchFiltersComponent implements OnInit {
     this.durationRange = undefined
     this.publishedDateRange = undefined
 
-    this.onInputUpdated()
+    this.onDurationOrPublishedUpdated()
   }
 
   resetField (fieldName: string, value?: any) {
@@ -136,7 +136,7 @@ export class SearchFiltersComponent implements OnInit {
 
   resetLocalField (fieldName: string, value?: any) {
     this[fieldName] = value
-    this.onInputUpdated()
+    this.onDurationOrPublishedUpdated()
   }
 
   resetOriginalPublicationYears () {
index b28abca6ac002eed4b538f2ad2cc297a86ee33a3..dc8b4d5951590898fabde323b23d699f8078aab1 100644 (file)
@@ -24,6 +24,8 @@
 
     <div class="results-filter collapse-transition" [ngbCollapse]="isSearchFilterCollapsed">
       <my-search-filters [advancedSearch]="advancedSearch" (filtered)="onFiltered()"></my-search-filters>
+
+      <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
     </div>
   </div>
 
index fca704d27762a3d84239216418e1e14465938bb1..b521825e56915037909d83b7be33a09bacac8d31 100644 (file)
   padding: 40px;
 }
 
+.alert-danger {
+  margin-top: 10px;
+}
+
 .results-header {
   font-size: 16px;
   padding-bottom: 20px;
index 235bbfa4c1660bdfc2dd13af3f48426b2e5c8391..7425b70166a0d3a1b194a4accbce1e4de8981018 100644 (file)
@@ -1,9 +1,10 @@
-import { forkJoin, of, Subscription } from 'rxjs'
+import { forkJoin, Subscription } from 'rxjs'
 import { LinkType } from 'src/types/link.type'
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { AuthService, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core'
 import { immutableAssign } from '@app/helpers'
+import { validateHost } from '@app/shared/form-validators/host-validators'
 import { Video, VideoChannel } from '@app/shared/shared-main'
 import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
 import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
@@ -16,7 +17,9 @@ import { HTMLServerConfig, SearchTargetType } from '@shared/models'
   templateUrl: './search.component.html'
 })
 export class SearchComponent implements OnInit, OnDestroy {
-  results: (Video | VideoChannel)[] = []
+  error: string
+
+  results: (Video | VideoChannel | VideoPlaylist)[] = []
 
   pagination = {
     currentPage: 1,
@@ -89,8 +92,10 @@ export class SearchComponent implements OnInit, OnDestroy {
           this.advancedSearch.searchTarget = this.getDefaultSearchTarget()
         }
 
-        // Don't hide filters if we have some of them AND the user just came on the webpage
-        this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues()
+        this.error = this.checkFieldsAndGetError()
+
+        // Don't hide filters if we have some of them AND the user just came on the webpage, or we have an error
+        this.isSearchFilterCollapsed = !this.error && (this.isInitialLoad === false || !this.advancedSearch.containsValues())
         this.isInitialLoad = false
 
         this.search()
@@ -126,6 +131,9 @@ export class SearchComponent implements OnInit, OnDestroy {
   }
 
   search () {
+    this.error = this.checkFieldsAndGetError()
+    if (this.error) return
+
     this.isSearching = true
 
     forkJoin([
@@ -275,12 +283,10 @@ export class SearchComponent implements OnInit, OnDestroy {
   }
 
   private getVideoChannelObs () {
-    if (!this.currentSearch) return of({ data: [], total: 0 })
-
     const params = {
       search: this.currentSearch,
       componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }),
-      searchTarget: this.advancedSearch.searchTarget
+      advancedSearch: this.advancedSearch
     }
 
     return this.hooks.wrapObsFun(
@@ -293,12 +299,10 @@ export class SearchComponent implements OnInit, OnDestroy {
   }
 
   private getVideoPlaylistObs () {
-    if (!this.currentSearch) return of({ data: [], total: 0 })
-
     const params = {
       search: this.currentSearch,
       componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }),
-      searchTarget: this.advancedSearch.searchTarget
+      advancedSearch: this.advancedSearch
     }
 
     return this.hooks.wrapObsFun(
@@ -319,4 +323,12 @@ export class SearchComponent implements OnInit, OnDestroy {
 
     return 'local'
   }
+
+  private checkFieldsAndGetError () {
+    if (this.advancedSearch.host && !validateHost(this.advancedSearch.host)) {
+      return $localize`PeerTube instance host filter is invalid`
+    }
+
+    return undefined
+  }
 }
index 3833d9c542c03a0509020d5e3c76b6b6914e0615..6479644f130b9df82892c32a23e1135254f7f64a 100644 (file)
@@ -7,7 +7,7 @@ import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService }
 import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
 import { SupportModalComponent } from '@app/shared/shared-support-modal'
 import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '@shared/models'
 
 @Component({
   templateUrl: './video-channels.component.html',
index 50d030ac9ce3b26922d29c6dc37d96a7af94c28b..ee5a506112d127612efd61b85755d43e52e32706 100644 (file)
@@ -45,7 +45,7 @@
                 </ng-template>
               </my-help>
 
-              <my-markdown-textarea [truncate]="250" formControlName="description" [markdownVideo]="true"></my-markdown-textarea>
+              <my-markdown-textarea [truncate]="250" formControlName="description" [markdownVideo]="videoToUpdate"></my-markdown-textarea>
 
               <div *ngIf="formErrors.description" class="form-error">
                 {{ formErrors.description }}
index d8d20a249dd23ebc801005eed4a083b263702030..189bc966977b20671385fc4524d18e42d4cfc211 100644 (file)
@@ -7,8 +7,7 @@ import { genericUploadErrorHandler, scrollToTop } from '@app/helpers'
 import { FormValidatorService } from '@app/shared/shared-forms'
 import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
 import { LoadingBarService } from '@ngx-loading-bar/core'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { VideoPrivacy } from '@shared/models'
+import { HttpStatusCode, VideoPrivacy } from '@shared/models'
 import { UploaderXFormData } from './uploaderx-form-data'
 import { VideoSend } from './video-send'
 
index 04f8f0d5882c0e5a30f07d4a647af6b9330c25e6..0e1c4c2070077fe3c747f79df363297f4c604e8f 100644 (file)
@@ -161,7 +161,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
     // Before HTML rendering restore line feed for markdown list compatibility
     const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n')
     const html = await this.markdownService.textMarkdownToHTML(commentText, true, true)
-    this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(html)
+    this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(this.video.shortUUID, html)
     this.newParentComments = this.parentComments.concat([ this.comment ])
 
     if (this.comment.account) {
index 598bc485d2cf4b82b409f30d51094d1e9779d466..362a2190514c2fe34c688693d98565d695f1471b 100644 (file)
@@ -1,54 +1,62 @@
-<div class="video-attribute">
-  <span i18n class="video-attribute-label">Privacy</span>
-  <span class="video-attribute-value">{{ video.privacy.label }}</span>
+<div class="attribute">
+  <span i18n class="attribute-label">Privacy</span>
+  <span class="attribute-value">{{ video.privacy.label }}</span>
 </div>
 
-<div *ngIf="video.isLocal === false" class="video-attribute">
-  <span i18n class="video-attribute-label">Origin</span>
-  <a class="video-attribute-value" target="_blank" rel="noopener noreferrer" [href]="getVideoUrl()">{{ video.originInstanceHost }}</a>
+<div *ngIf="video.isLocal === false" class="attribute">
+  <span i18n class="attribute-label">Origin</span>
+  <a
+    class="attribute-value" target="_blank" rel="noopener noreferrer"
+    routerLink="/search" [queryParams]="{ host: getVideoHost() }"
+  >{{ video.originInstanceHost }}</a>
+
+  <a
+    i18n-title title="Open the video on the origin instance" class="glyphicon glyphicon-new-window"
+    target="_blank" rel="noopener noreferrer" [href]="getVideoUrl()"
+  ></a>
 </div>
 
-<div *ngIf="!!video.originallyPublishedAt" class="video-attribute">
-  <span i18n class="video-attribute-label">Originally published</span>
-  <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span>
+<div *ngIf="!!video.originallyPublishedAt" class="attribute">
+  <span i18n class="attribute-label">Originally published</span>
+  <span class="attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span>
 </div>
 
-<div class="video-attribute">
-  <span i18n class="video-attribute-label">Category</span>
-  <span *ngIf="!video.category.id" class="video-attribute-value">{{ video.category.label }}</span>
+<div class="attribute">
+  <span i18n class="attribute-label">Category</span>
+  <span *ngIf="!video.category.id" class="attribute-value">{{ video.category.label }}</span>
   <a
-    *ngIf="video.category.id" class="video-attribute-value"
+    *ngIf="video.category.id" class="attribute-value"
     [routerLink]="[ '/search' ]" [queryParams]="{ categoryOneOf: [ video.category.id ] }"
   >{{ video.category.label }}</a>
 </div>
 
-<div class="video-attribute">
-  <span i18n class="video-attribute-label">Licence</span>
-  <span *ngIf="!video.licence.id" class="video-attribute-value">{{ video.licence.label }}</span>
+<div class="attribute">
+  <span i18n class="attribute-label">Licence</span>
+  <span *ngIf="!video.licence.id" class="attribute-value">{{ video.licence.label }}</span>
   <a
-    *ngIf="video.licence.id" class="video-attribute-value"
+    *ngIf="video.licence.id" class="attribute-value"
     [routerLink]="[ '/search' ]" [queryParams]="{ licenceOneOf: [ video.licence.id ] }"
   >{{ video.licence.label }}</a>
 </div>
 
-<div class="video-attribute">
-  <span i18n class="video-attribute-label">Language</span>
-  <span *ngIf="!video.language.id" class="video-attribute-value">{{ video.language.label }}</span>
+<div class="attribute">
+  <span i18n class="attribute-label">Language</span>
+  <span *ngIf="!video.language.id" class="attribute-value">{{ video.language.label }}</span>
   <a
-    *ngIf="video.language.id" class="video-attribute-value"
+    *ngIf="video.language.id" class="attribute-value"
     [routerLink]="[ '/search' ]" [queryParams]="{ languageOneOf: [ video.language.id ] }"
   >{{ video.language.label }}</a>
 </div>
 
-<div class="video-attribute video-attribute-tags">
-  <span i18n class="video-attribute-label">Tags</span>
+<div class="attribute attribute-tags">
+  <span i18n class="attribute-label">Tags</span>
   <a
     *ngFor="let tag of getVideoTags()"
-    class="video-attribute-value" [routerLink]="[ '/search' ]" [queryParams]="{ tagsOneOf: [ tag ] }"
+    class="attribute-value" [routerLink]="[ '/search' ]" [queryParams]="{ tagsOneOf: [ tag ] }"
   >{{ tag }}</a>
 </div>
 
-<div class="video-attribute" *ngIf="!video.isLive">
-  <span i18n class="video-attribute-label">Duration</span>
-  <span class="video-attribute-value">{{ video.duration | myDurationFormatter }}</span>
+<div class="attribute" *ngIf="!video.isLive">
+  <span i18n class="attribute-label">Duration</span>
+  <span class="attribute-value">{{ video.duration | myDurationFormatter }}</span>
 </div>
index 45190a3e37d671730ca8ed026bd0909aae78f2ab..26bead12471c53953dac6249f079d5413dd06fcf 100644 (file)
@@ -1,13 +1,13 @@
 @use '_variables' as *;
 @use '_mixins' as *;
 
-.video-attribute {
+.attribute {
   font-size: 13px;
   display: block;
   margin-bottom: 12px;
 }
 
-.video-attribute-label {
+.attribute-label {
   @include padding-right(5px);
 
   min-width: 142px;
@@ -16,7 +16,7 @@
   font-weight: $font-bold;
 }
 
-a.video-attribute-value {
+a.attribute-value {
   @include disable-default-a-behaviour;
   color: pvar(--mainForegroundColor);
 
@@ -25,16 +25,22 @@ a.video-attribute-value {
   }
 }
 
-.video-attribute-tags {
-  .video-attribute-value:not(:nth-child(2)) {
+.attribute-tags {
+  .attribute-value:not(:nth-child(2)) {
     &::before {
       content: ', ';
     }
   }
 }
 
+.glyphicon-new-window {
+  color: pvar(--inputPlaceholderColor);
+  margin-left: 5px;
+  font-size: 12px;
+}
+
 @media screen and (max-width: 1600px) {
-  .video-attributes .video-attribute {
+  .attributes .attribute {
     margin-bottom: 5px;
   }
 }
index 5cb77f0c86ed4455248089167340906eee1a8ab6..9429581ac7ce6ebbe386ae56763f26a5baa092a8 100644 (file)
@@ -17,6 +17,10 @@ export class VideoAttributesComponent {
     return this.video.url
   }
 
+  getVideoHost () {
+    return this.video.channel.host
+  }
+
   getVideoTags () {
     if (!this.video || Array.isArray(this.video.tags) === false) return []
 
index 23d00d31a09d7268473b683a38b6f8e5c761379e..870c7ae3f6520ebebc44c5368fb073a7cef74f68 100644 (file)
@@ -80,6 +80,7 @@ export class VideoDescriptionComponent implements OnChanges {
 
   private async setVideoDescriptionHTML () {
     const html = await this.markdownService.textMarkdownToHTML(this.video.description)
-    this.videoHTMLDescription = this.markdownService.processVideoTimestamps(html)
+
+    this.videoHTMLDescription = this.markdownService.processVideoTimestamps(this.video.shortUUID, html)
   }
 }
index d078844c3e1058ee2c29e8082bf9b6df17a2a994..ccb9c5e718e7a384e64a15484f01bda27c4fb637 100644 (file)
@@ -21,8 +21,16 @@ import { isXPercentInViewport, scrollToTop } from '@app/helpers'
 import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
 import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
 import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { HTMLServerConfig, PeerTubeProblemDocument, ServerErrorCode, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
+import { timeToInt } from '@shared/core-utils'
+import {
+  HTMLServerConfig,
+  HttpStatusCode,
+  PeerTubeProblemDocument,
+  ServerErrorCode,
+  VideoCaption,
+  VideoPrivacy,
+  VideoState
+} from '@shared/models'
 import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
 import {
   CustomizationOptions,
@@ -32,7 +40,6 @@ import {
   PlayerMode,
   videojs
 } from '../../../assets/player/peertube-player-manager'
-import { timeToInt } from '../../../assets/player/utils'
 import { environment } from '../../../environments/environment'
 import { VideoWatchPlaylistComponent } from './shared'
 
@@ -575,6 +582,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 
         videoCaptions: playerCaptions,
 
+        videoShortUUID: video.shortUUID,
         videoUUID: video.uuid
       },
 
index cdf13186b68f3338dc49ba687f0eed411c1bf0f8..60bd72c6075dc5b9553c2f0ee81f62e5959bca9a 100644 (file)
@@ -6,12 +6,11 @@ import { Injectable } from '@angular/core'
 import { Router } from '@angular/router'
 import { Notifier } from '@app/core/notification/notifier.service'
 import { objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index'
-import { MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models'
+import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models'
 import { environment } from '../../../environments/environment'
 import { RestExtractor } from '../rest/rest-extractor.service'
 import { AuthStatus } from './auth-status.model'
 import { AuthUser } from './auth-user.model'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
 
 interface UserLoginWithUsername extends UserLogin {
   access_token: string
index 60130382fdb91de4aff135b02407ce813a867762..0b8d0191e15dfe113aacce005c5cab250c106886 100644 (file)
@@ -101,7 +101,7 @@ export class MenuService {
 
     return {
       key: 'in-my-library',
-      title: 'In my library',
+      title: $localize`In my library`,
       links
     }
   }
index ca1bf4eb95416515bfeb1ebaec3e6191d3cbb5b2..36258ca9872f6f1a471b9ac83c8b57f5341a0148 100644 (file)
@@ -1,6 +1,6 @@
 import * as MarkdownIt from 'markdown-it'
-import { buildVideoLink } from 'src/assets/player/utils'
 import { Injectable } from '@angular/core'
+import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
 import {
   COMPLETE_RULES,
   ENHANCED_RULES,
@@ -82,10 +82,14 @@ export class MarkdownService {
     return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags })
   }
 
-  processVideoTimestamps (html: string) {
+  processVideoTimestamps (videoShortUUID: string, html: string) {
     return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) {
       const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0))
-      const url = buildVideoLink({ startTime: t })
+
+      const url = decorateVideoLink({
+        url: buildVideoLink({ shortUUID: videoShortUUID }),
+        startTime: t
+      })
       return `<a class="video-timestamp" href="${url}">${str}</a>`
     })
   }
index 08ab495124885f3fe2f2f2210a28d94cf2cc5fdc..2a926e68f10c286f0618193742949eb41d04d326 100644 (file)
@@ -2,8 +2,7 @@ import { throwError as observableThrowError } from 'rxjs'
 import { Injectable } from '@angular/core'
 import { Router } from '@angular/router'
 import { dateToHuman } from '@app/helpers'
-import { ResultList } from '@shared/models'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode, ResultList } from '@shared/models'
 
 @Injectable()
 export class RestExtractor {
index 94f6def2676c0b84a907e9f8fe8cb48067aaa6db..edcaf50e00b4b77a3f0d74e6f76fc52c0813b4c4 100644 (file)
@@ -3,7 +3,7 @@ import { SelectChannelItem } from 'src/types/select-options-item.model'
 import { DatePipe } from '@angular/common'
 import { HttpErrorResponse } from '@angular/common/http'
 import { Notifier } from '@app/core'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '@shared/models'
 import { environment } from '../../environments/environment'
 import { AuthService } from '../core/auth'
 
diff --git a/client/src/app/shared/form-validators/batch-domains-validators.ts b/client/src/app/shared/form-validators/batch-domains-validators.ts
deleted file mode 100644 (file)
index 423d133..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-import { AbstractControl, FormControl, ValidatorFn, Validators } from '@angular/forms'
-import { BuildFormValidator } from './form-validator.model'
-import { validateHost } from './host'
-
-export function getNotEmptyHosts (hosts: string) {
-  return hosts
-    .split('\n')
-    .filter((host: string) => host && host.length !== 0) // Eject empty hosts
-}
-
-const validDomains: ValidatorFn = (control: FormControl) => {
-  if (!control.value) return null
-
-  const newHostsErrors = []
-  const hosts = getNotEmptyHosts(control.value)
-
-  for (const host of hosts) {
-    if (validateHost(host) === false) {
-      newHostsErrors.push($localize`${host} is not valid`)
-    }
-  }
-
-  /* Is not valid. */
-  if (newHostsErrors.length !== 0) {
-    return {
-      'validDomains': {
-        reason: 'invalid',
-        value: newHostsErrors.join('. ') + '.'
-      }
-    }
-  }
-
-  /* Is valid. */
-  return null
-}
-
-const isHostsUnique: ValidatorFn = (control: AbstractControl) => {
-  if (!control.value) return null
-
-  const hosts = getNotEmptyHosts(control.value)
-
-  if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) {
-    return null
-  } else {
-    return {
-      'uniqueDomains': {
-        reason: 'invalid'
-      }
-    }
-  }
-}
-
-export const DOMAINS_VALIDATOR: BuildFormValidator = {
-  VALIDATORS: [Validators.required, validDomains, isHostsUnique],
-  MESSAGES: {
-    'required': $localize`Domain is required.`,
-    'validDomains': $localize`Domains entered are invalid.`,
-    'uniqueDomains': $localize`Domains entered contain duplicates.`
-  }
-}
diff --git a/client/src/app/shared/form-validators/host-validators.ts b/client/src/app/shared/form-validators/host-validators.ts
new file mode 100644 (file)
index 0000000..6f410a5
--- /dev/null
@@ -0,0 +1,105 @@
+import { AbstractControl, ValidatorFn, Validators } from '@angular/forms'
+import { BuildFormValidator } from './form-validator.model'
+
+export function validateHost (value: string) {
+  // Thanks to http://stackoverflow.com/a/106223
+  const HOST_REGEXP = new RegExp(
+    '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
+  )
+
+  return HOST_REGEXP.test(value)
+}
+
+export function validateHandle (value: string) {
+  if (!value) return false
+
+  return value.includes('@')
+}
+
+const validHosts: ValidatorFn = (control: AbstractControl) => {
+  if (!control.value) return null
+
+  const errors = []
+  const hosts = splitAndGetNotEmpty(control.value)
+
+  for (const host of hosts) {
+    if (validateHost(host) === false) {
+      errors.push($localize`${host} is not valid`)
+    }
+  }
+
+  // valid
+  if (errors.length === 0) return null
+
+  return {
+    'validHosts': {
+      reason: 'invalid',
+      value: errors.join('. ') + '.'
+    }
+  }
+}
+
+const validHostsOrHandles: ValidatorFn = (control: AbstractControl) => {
+  if (!control.value) return null
+
+  const errors = []
+  const lines = splitAndGetNotEmpty(control.value)
+
+  for (const line of lines) {
+    if (validateHost(line) === false && validateHandle(line) === false) {
+      errors.push($localize`${line} is not valid`)
+    }
+  }
+
+  // valid
+  if (errors.length === 0) return null
+
+  return {
+    'validHostsOrHandles': {
+      reason: 'invalid',
+      value: errors.join('. ') + '.'
+    }
+  }
+}
+
+// ---------------------------------------------------------------------------
+
+export function splitAndGetNotEmpty (value: string) {
+  return value
+    .split('\n')
+    .filter(line => line && line.length !== 0) // Eject empty hosts
+}
+
+export const unique: ValidatorFn = (control: AbstractControl) => {
+  if (!control.value) return null
+
+  const hosts = splitAndGetNotEmpty(control.value)
+
+  if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) {
+    return null
+  }
+
+  return {
+    'unique': {
+      reason: 'invalid'
+    }
+  }
+}
+
+export const UNIQUE_HOSTS_VALIDATOR: BuildFormValidator = {
+  VALIDATORS: [ Validators.required, validHosts, unique ],
+  MESSAGES: {
+    'required': $localize`Domain is required.`,
+    'validHosts': $localize`Hosts entered are invalid.`,
+    'unique': $localize`Hosts entered contain duplicates.`
+  }
+}
+
+export const UNIQUE_HOSTS_OR_HANDLE_VALIDATOR: BuildFormValidator = {
+  VALIDATORS: [ Validators.required, validHostsOrHandles, unique ],
+  MESSAGES: {
+    'required': $localize`Domain is required.`,
+    'validHostsOrHandles': $localize`Hosts or handles are invalid.`,
+    'unique': $localize`Hosts or handles contain duplicates.`
+  }
+}
diff --git a/client/src/app/shared/form-validators/host.ts b/client/src/app/shared/form-validators/host.ts
deleted file mode 100644 (file)
index c18a35f..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-export function validateHost (value: string) {
-  // Thanks to http://stackoverflow.com/a/106223
-  const HOST_REGEXP = new RegExp(
-    '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
-  )
-
-  return HOST_REGEXP.test(value)
-}
index f621f03a43d6facf88b9a48a18e1ec95a70968f5..0b605719c1d4e9c85494aa0a98c708936a470b64 100644 (file)
@@ -1,7 +1,6 @@
 export * from './form-validator.model'
-export * from './host'
 
-// Don't re export const variables because webpack cannot do tree shaking with them
+// Don't re export const variables because webpack cannot do tree shaking with them
 // export * from './abuse-validators'
 // export * from './batch-domains-validators'
 // export * from './custom-config-validators'
index 67aa0e39955fcc70d15f562da733843b0cc51975..a7932ebabd6127e93edd0e2020f24b0c33c8b7cd 100644 (file)
@@ -1,7 +1,7 @@
 import * as debug from 'debug'
 import truncate from 'lodash-es/truncate'
 import { SortMeta } from 'primeng/api'
-import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
+import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
 import { environment } from 'src/environments/environment'
 import { Component, Input, OnInit, ViewChild } from '@angular/core'
 import { DomSanitizer } from '@angular/platform-browser'
@@ -10,6 +10,7 @@ import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable }
 import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
 import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
 import { VideoCommentService } from '@app/shared/shared-video-comment'
+import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils'
 import { AbuseState, AdminAbuse } from '@shared/models'
 import { AdvancedInputFilter } from '../shared-forms'
 import { AbuseMessageModalComponent } from './abuse-message-modal.component'
@@ -129,8 +130,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
 
   getVideoEmbed (abuse: AdminAbuse) {
     return buildVideoOrPlaylistEmbed(
-      buildVideoLink({
-        baseUrl: `${environment.originServerUrl}/videos/embed/${abuse.video.uuid}`,
+      decorateVideoLink({
+        url: buildVideoEmbedLink(abuse.video, environment.originServerUrl),
         title: false,
         warningTitle: false,
         startTime: abuse.video.startAt,
index 4462903db696cbb444901dfa21d2d69cc1dec293..53b70cc4759aeaf714ada1dcf01cdfa7a51120b0 100644 (file)
@@ -1,6 +1,7 @@
-import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
+import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
 import { environment } from 'src/environments/environment'
 import { Component, ElementRef, Input, OnInit } from '@angular/core'
+import { buildPlaylistEmbedLink, buildVideoEmbedLink } from '@shared/core-utils'
 import { CustomMarkupComponent } from './shared'
 
 @Component({
@@ -17,8 +18,8 @@ export class EmbedMarkupComponent implements CustomMarkupComponent, OnInit {
 
   ngOnInit () {
     const link = this.type === 'video'
-      ? buildVideoLink({ baseUrl: `${environment.originServerUrl}/videos/embed/${this.uuid}` })
-      : buildPlaylistLink({ baseUrl: `${environment.originServerUrl}/video-playlists/embed/${this.uuid}` })
+      ? buildVideoEmbedLink({ uuid: this.uuid }, environment.originServerUrl)
+      : buildPlaylistEmbedLink({ uuid: this.uuid }, environment.originServerUrl)
 
     this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid)
   }
index a233a42050cb348daa5137742a3bfacb349565bf..8f51d47df638a7ae21f869a1423ba043fc996f80 100644 (file)
@@ -6,6 +6,7 @@ import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@an
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
 import { SafeHtml } from '@angular/platform-browser'
 import { MarkdownService, ScreenService } from '@app/core'
+import { Video } from '@shared/models'
 
 @Component({
   selector: 'my-markdown-textarea',
@@ -33,7 +34,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
   @Input() markdownType: 'text' | 'enhanced' = 'text'
   @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement>
 
-  @Input() markdownVideo = false
+  @Input() markdownVideo: Video
 
   @Input() name = 'description'
 
@@ -147,7 +148,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
     }
 
     if (this.markdownVideo) {
-      html = this.markdownService.processVideoTimestamps(html)
+      html = this.markdownService.processVideoTimestamps(this.markdownVideo.shortUUID, html)
     }
 
     return html
index 0ffd03d02886082da3e8519751a0278997d313a1..3fc705905e267b5b48f3de21b89310595f72dd90 100644 (file)
@@ -1,6 +1,6 @@
 import { ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
-import { secondsToTime, timeToInt } from '../../../assets/player/utils'
+import { secondsToTime, timeToInt } from '@shared/core-utils'
 
 @Component({
   selector: 'my-timestamp-input',
index e5266014067f2814643a620f1b64d7f991096260..af44020cf4ef1df622ea2fe64fa07b9d941e1864 100644 (file)
@@ -4,7 +4,7 @@ import { catchError, map } from 'rxjs/operators'
 import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { RestExtractor, RestPagination, RestService } from '@app/core'
-import { ActivityPubActorType, ActorFollow, FollowState, ResultList } from '@shared/models'
+import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models'
 import { environment } from '../../../environments/environment'
 
 @Injectable()
@@ -64,9 +64,10 @@ export class InstanceFollowService {
                )
   }
 
-  follow (notEmptyHosts: string[]) {
-    const body = {
-      hosts: notEmptyHosts
+  follow (hostsOrHandles: string[]) {
+    const body: ServerFollowCreate = {
+      handles: hostsOrHandles.filter(v => v.includes('@')),
+      hosts: hostsOrHandles.filter(v => !v.includes('@'))
     }
 
     return this.authHttp.post(InstanceFollowService.BASE_APPLICATION_URL + '/following', body)
@@ -77,7 +78,9 @@ export class InstanceFollowService {
   }
 
   unfollow (follow: ActorFollow) {
-    return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + follow.following.host)
+    const handle = follow.following.name + '@' + follow.following.host
+
+    return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + handle)
                .pipe(
                  map(this.restExtractor.extractDataBool),
                  catchError(res => this.restExtractor.handleError(res))
index 5bcad36d01d97e2b5ab11cb877774f4ce7f7157a..a75c8a25c7c29b3dd797829c50f6ad4011b5c0c2 100644 (file)
@@ -1,11 +1,11 @@
 import { Observable, of, throwError as observableThrowError } from 'rxjs'
 import { catchError, switchMap } from 'rxjs/operators'
-import { HTTP_INTERCEPTORS, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpErrorResponse } from '@angular/common/http'
+import { HTTP_INTERCEPTORS, HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'
 import { Injectable, Injector } from '@angular/core'
-import { AuthService } from '@app/core/auth/auth.service'
 import { Router } from '@angular/router'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { OAuth2ErrorCode, PeerTubeProblemDocument, ServerErrorCode } from '@shared/models/server'
+import { AuthService } from '@app/core/auth/auth.service'
+import { HttpStatusCode } from '@shared/models'
+import { OAuth2ErrorCode, PeerTubeProblemDocument } from '@shared/models/server'
 
 @Injectable()
 export class AuthInterceptor implements HttpInterceptor {
index 4c15eb98186ecc5fbdd0bc0f398ef60344d698dc..43954710270fdf2546561729bce0da71f5be705a 100644 (file)
@@ -47,11 +47,7 @@ export class UserNotification implements UserNotificationServer {
     comment?: {
       threadId: number
 
-      video: {
-        id: number
-        uuid: string
-        name: string
-      }
+      video: VideoInfo
     }
 
     account?: ActorInfo
index d7c72235528ddef83bea36ac3ac9b82de4b096d3..96b141543d04758ead52dee960d7d8de3a10dd35 100644 (file)
@@ -1,7 +1,7 @@
 import { Subject } from 'rxjs'
 import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
 import { ComponentPagination, hasMoreItems, Notifier } from '@app/core'
-import { UserNotificationType, AbuseState } from '@shared/models'
+import { AbuseState } from '@shared/models'
 import { UserNotification } from './user-notification.model'
 import { UserNotificationService } from './user-notification.service'
 
index f0a4a3f37c031a9a7b33797d31ed2209e1750ec5..b7720c8d21da3617869c159a391f2d1be60a5f6a 100644 (file)
@@ -2,6 +2,7 @@ import { AuthUser } from '@app/core'
 import { User } from '@app/core/users/user.model'
 import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
 import { Actor } from '@app/shared/shared-main/account/actor.model'
+import { buildVideoWatchPath } from '@shared/core-utils'
 import { peertubeTranslate } from '@shared/core-utils/i18n'
 import {
   ActorImage,
@@ -92,7 +93,7 @@ export class Video implements VideoServerModel {
   pluginData?: any
 
   static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
-    return '/w/' + (video.shortUUID || video.uuid)
+    return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
   }
 
   static buildUpdateUrl (video: Pick<Video, 'uuid'>) {
index 6a3c657213606f013aff827fcb89257d6b56c148..8306a96bce3508fcaaf988f0c1d54dd6208c4652 100644 (file)
@@ -1,6 +1,6 @@
 <ng-template #modal>
   <div class="modal-header">
-    <h4 i18n class="modal-title">{{ action }}</h4>
+    <h4 class="modal-title">{{ action }}</h4>
 
     <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
   </div>
         <label i18n for="hosts">1 host (without "http://") per line</label>
 
         <textarea
-          [placeholder]="placeholder" formControlName="domains" type="text" id="hosts" name="hosts"
-          class="form-control" [ngClass]="{ 'input-error': formErrors['domains'] }" ngbAutofocus
+          [placeholder]="placeholder" formControlName="hosts" type="text" id="hosts" name="hosts"
+          class="form-control" [ngClass]="{ 'input-error': formErrors['hosts'] }" ngbAutofocus
         ></textarea>
 
-        <div *ngIf="formErrors.domains" class="form-error">
-          {{ formErrors.domains }}
+        <div *ngIf="formErrors.hosts" class="form-error">
+          {{ formErrors.hosts }}
 
-          <div *ngIf="form.controls['domains'].errors.validDomains">
-            {{ form.controls['domains'].errors.validDomains.value }}
+          <div *ngIf="form.controls['hosts'].errors.validHosts">
+            {{ form.controls['hosts'].errors.validHosts.value }}
           </div>
         </div>
       </div>
index 6edbb602328380ec11dbab454554818b233d3659..20be728f6c9bcabfb8a258aff4736abf143b1c8b 100644 (file)
@@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angu
 import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
-import { DOMAINS_VALIDATOR, getNotEmptyHosts } from '../form-validators/batch-domains-validators'
+import { splitAndGetNotEmpty, UNIQUE_HOSTS_VALIDATOR } from '../form-validators/host-validators'
 
 @Component({
   selector: 'my-batch-domains-modal',
@@ -28,7 +28,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit {
     if (!this.action) this.action = $localize`Process domains`
 
     this.buildForm({
-      domains: DOMAINS_VALIDATOR
+      hosts: UNIQUE_HOSTS_VALIDATOR
     })
   }
 
@@ -41,9 +41,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit {
   }
 
   submit () {
-    this.domains.emit(
-      getNotEmptyHosts(this.form.controls['domains'].value)
-    )
+    this.domains.emit(splitAndGetNotEmpty(this.form.controls['hosts'].value))
     this.form.reset()
     this.hide()
   }
index 4ca6f52ad499887872b7beb34f7dd98484580ba6..e509ac88f124594f6f51e85017f3342de9d790ca 100644 (file)
@@ -1,5 +1,5 @@
 import { mapValues, pickBy } from 'lodash-es'
-import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
+import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
 import { Component, Input, OnInit, ViewChild } from '@angular/core'
 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
 import { Notifier } from '@app/core'
@@ -7,6 +7,7 @@ import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-valida
 import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
+import { decorateVideoLink } from '@shared/core-utils'
 import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
 import { AbusePredefinedReasonsString } from '@shared/models'
 import { Video } from '../../shared-main'
@@ -57,11 +58,12 @@ export class VideoReportComponent extends FormReactive implements OnInit {
   getVideoEmbed () {
     return this.sanitizer.bypassSecurityTrustHtml(
       buildVideoOrPlaylistEmbed(
-        buildVideoLink({
-          baseUrl: this.video.embedUrl,
+        decorateVideoLink({
+          url: this.video.embedUrl,
           title: false,
           warningTitle: false
         }),
+
         this.video.name
       )
     )
index 2c83f53b64c3a12d611b2a10ee2478b76f23fa83..9c55f6cd853e0b68d9560911718108e39f707427 100644 (file)
@@ -1,4 +1,11 @@
-import { BooleanBothQuery, BooleanQuery, SearchTargetType, VideosSearchQuery } from '@shared/models'
+import {
+  BooleanBothQuery,
+  BooleanQuery,
+  SearchTargetType,
+  VideoChannelsSearchQuery,
+  VideoPlaylistsSearchQuery,
+  VideosSearchQuery
+} from '@shared/models'
 
 export class AdvancedSearch {
   startDate: string // ISO 8601
@@ -23,6 +30,8 @@ export class AdvancedSearch {
 
   isLive: BooleanQuery
 
+  host: string
+
   sort: string
 
   searchTarget: SearchTargetType
@@ -45,6 +54,8 @@ export class AdvancedSearch {
 
     isLive?: BooleanQuery
 
+    host?: string
+
     durationMin?: string
     durationMax?: string
     sort?: string
@@ -68,6 +79,8 @@ export class AdvancedSearch {
     this.durationMin = parseInt(options.durationMin, 10)
     this.durationMax = parseInt(options.durationMax, 10)
 
+    this.host = options.host || undefined
+
     this.searchTarget = options.searchTarget || undefined
 
     if (isNaN(this.durationMin)) this.durationMin = undefined
@@ -101,6 +114,7 @@ export class AdvancedSearch {
     this.durationMin = undefined
     this.durationMax = undefined
     this.isLive = undefined
+    this.host = undefined
 
     this.sort = '-match'
   }
@@ -120,12 +134,13 @@ export class AdvancedSearch {
       durationMin: this.durationMin,
       durationMax: this.durationMax,
       isLive: this.isLive,
+      host: this.host,
       sort: this.sort,
       searchTarget: this.searchTarget
     }
   }
 
-  toAPIObject (): VideosSearchQuery {
+  toVideosAPIObject (): VideosSearchQuery {
     let isLive: boolean
     if (this.isLive) isLive = this.isLive === 'true'
 
@@ -142,12 +157,27 @@ export class AdvancedSearch {
       tagsAllOf: this.tagsAllOf,
       durationMin: this.durationMin,
       durationMax: this.durationMax,
+      host: this.host,
       isLive,
       sort: this.sort,
       searchTarget: this.searchTarget
     }
   }
 
+  toPlaylistAPIObject (): VideoPlaylistsSearchQuery {
+    return {
+      host: this.host,
+      searchTarget: this.searchTarget
+    }
+  }
+
+  toChannelAPIObject (): VideoChannelsSearchQuery {
+    return {
+      host: this.host,
+      searchTarget: this.searchTarget
+    }
+  }
+
   size () {
     let acc = 0
 
index ad258f5e5b89c19579ad462619598f6d47ffd633..a1603da983b5f0e54549d9e8214f65e56f8ce03a 100644 (file)
@@ -7,7 +7,6 @@ import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/sha
 import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
 import {
   ResultList,
-  SearchTargetType,
   Video as VideoServerModel,
   VideoChannel as VideoChannelServerModel,
   VideoPlaylist as VideoPlaylistServerModel
@@ -33,8 +32,8 @@ export class SearchService {
   }
 
   searchVideos (parameters: {
-    search: string,
-    componentPagination?: ComponentPaginationLight,
+    search: string
+    componentPagination?: ComponentPaginationLight
     advancedSearch?: AdvancedSearch
   }): Observable<ResultList<Video>> {
     const { search, componentPagination, advancedSearch } = parameters
@@ -52,7 +51,7 @@ export class SearchService {
     if (search) params = params.append('search', search)
 
     if (advancedSearch) {
-      const advancedSearchObject = advancedSearch.toAPIObject()
+      const advancedSearchObject = advancedSearch.toVideosAPIObject()
       params = this.restService.addObjectParams(params, advancedSearchObject)
     }
 
@@ -65,11 +64,11 @@ export class SearchService {
   }
 
   searchVideoChannels (parameters: {
-    search: string,
-    searchTarget?: SearchTargetType,
+    search: string
+    advancedSearch?: AdvancedSearch
     componentPagination?: ComponentPaginationLight
   }): Observable<ResultList<VideoChannel>> {
-    const { search, componentPagination, searchTarget } = parameters
+    const { search, advancedSearch, componentPagination } = parameters
 
     const url = SearchService.BASE_SEARCH_URL + 'video-channels'
 
@@ -80,10 +79,12 @@ export class SearchService {
 
     let params = new HttpParams()
     params = this.restService.addRestGetParams(params, pagination)
-    params = params.append('search', search)
 
-    if (searchTarget) {
-      params = params.append('searchTarget', searchTarget as string)
+    if (search) params = params.append('search', search)
+
+    if (advancedSearch) {
+      const advancedSearchObject = advancedSearch.toChannelAPIObject()
+      params = this.restService.addObjectParams(params, advancedSearchObject)
     }
 
     return this.authHttp
@@ -95,11 +96,11 @@ export class SearchService {
   }
 
   searchVideoPlaylists (parameters: {
-    search: string,
-    searchTarget?: SearchTargetType,
+    search: string
+    advancedSearch?: AdvancedSearch
     componentPagination?: ComponentPaginationLight
   }): Observable<ResultList<VideoPlaylist>> {
-    const { search, componentPagination, searchTarget } = parameters
+    const { search, advancedSearch, componentPagination } = parameters
 
     const url = SearchService.BASE_SEARCH_URL + 'video-playlists'
 
@@ -110,10 +111,12 @@ export class SearchService {
 
     let params = new HttpParams()
     params = this.restService.addRestGetParams(params, pagination)
-    params = params.append('search', search)
 
-    if (searchTarget) {
-      params = params.append('searchTarget', searchTarget as string)
+    if (search) params = params.append('search', search)
+
+    if (advancedSearch) {
+      const advancedSearchObject = advancedSearch.toPlaylistAPIObject()
+      params = this.restService.addObjectParams(params, advancedSearchObject)
     }
 
     return this.authHttp
index a41ff248b1e6ea3ce954a955bfcbc95025b3e23c..341abdc2b7eb0a8deac72a165db282a4309ba966 100644 (file)
@@ -1,9 +1,10 @@
 import { Component, ElementRef, Input, ViewChild } from '@angular/core'
-import { Video, VideoDetails } from '@app/shared/shared-main'
+import { VideoDetails } from '@app/shared/shared-main'
 import { VideoPlaylist } from '@app/shared/shared-video-playlist'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { buildPlaylistLink, buildVideoLink, decoratePlaylistLink, decorateVideoLink } from '@shared/core-utils'
 import { VideoCaption } from '@shared/models'
-import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from '../../../assets/player/utils'
+import { buildVideoOrPlaylistEmbed } from '../../../assets/player/utils'
 
 type Customizations = {
   startAtCheckbox: boolean
@@ -83,34 +84,34 @@ export class VideoShareComponent {
   }
 
   getVideoIframeCode () {
-    const options = this.getVideoOptions(this.video.embedUrl)
+    const embedUrl = decorateVideoLink({ url: this.video.embedUrl, ...this.getVideoOptions() })
 
-    const embedUrl = buildVideoLink(options)
     return buildVideoOrPlaylistEmbed(embedUrl, this.video.name)
   }
 
   getPlaylistIframeCode () {
-    const options = this.getPlaylistOptions(this.playlist.embedUrl)
+    const embedUrl = decoratePlaylistLink({ url: this.playlist.embedUrl, ...this.getPlaylistOptions() })
 
-    const embedUrl = buildPlaylistLink(options)
     return buildVideoOrPlaylistEmbed(embedUrl, this.playlist.displayName)
   }
 
   getVideoUrl () {
-    let baseUrl = this.customizations.originUrl ? this.video.originInstanceUrl : window.location.origin
-    baseUrl += Video.buildWatchUrl(this.video)
+    const baseUrl = this.customizations.originUrl
+      ? this.video.originInstanceUrl
+      : window.location.origin
 
-    const options = this.getVideoOptions(baseUrl)
+    return decorateVideoLink({
+      url: buildVideoLink(this.video, baseUrl),
 
-    return buildVideoLink(options)
+      ...this.getVideoOptions()
+    })
   }
 
   getPlaylistUrl () {
-    const base = window.location.origin + VideoPlaylist.buildWatchUrl(this.playlist)
+    const url = buildPlaylistLink(this.playlist)
+    if (!this.includeVideoInPlaylist) return url
 
-    if (!this.includeVideoInPlaylist) return base
-
-    return base + '?playlistPosition=' + this.playlistPosition
+    return decoratePlaylistLink({ url, playlistPosition: this.playlistPosition })
   }
 
   notSecure () {
@@ -133,10 +134,8 @@ export class VideoShareComponent {
     }
   }
 
-  private getVideoOptions (baseUrl?: string) {
+  private getVideoOptions () {
     return {
-      baseUrl,
-
       startTime: this.customizations.startAtCheckbox ? this.customizations.startAt : undefined,
       stopTime: this.customizations.stopAtCheckbox ? this.customizations.stopAt : undefined,
 
index 52e72d35b8833cfe7463d9af7ea94ad6052e8887..33061a837509276b5a8b0122293b86fa199bf3bf 100644 (file)
@@ -24,7 +24,7 @@ import {
 } from '@app/core'
 import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
 import { GlobalIconName } from '@app/shared/shared-icons'
-import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@shared/core-utils/miscs/date'
+import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@shared/core-utils'
 import { HTMLServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models'
 import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
 import { Syndication, Video } from '../shared-main'
index 681e5becd0e40e84704375cce29fbbbb3d7e83d9..8b019103c2c41fa4949d596adff4d79b4d09ca9f 100644 (file)
@@ -4,6 +4,7 @@ import { debounceTime, filter } from 'rxjs/operators'
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'
 import { AuthService, DisableForReuseHook, Notifier } from '@app/core'
 import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
+import { secondsToTime } from '@shared/core-utils'
 import {
   Video,
   VideoExistInPlaylist,
@@ -12,7 +13,6 @@ import {
   VideoPlaylistElementUpdate,
   VideoPlaylistPrivacy
 } from '@shared/models'
-import { secondsToTime } from '../../../assets/player/utils'
 import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators'
 import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service'
 
index d99170e4e236913b4fcade137612778912bca6fa..2e495ec266c7a95e8a472df0840947690f4b9195 100644 (file)
@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, In
 import { AuthService, Notifier, ServerService } from '@app/core'
 import { Video } from '@app/shared/shared-main'
 import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
+import { secondsToTime } from '@shared/core-utils'
 import { HTMLServerConfig, VideoPlaylistElementType, VideoPlaylistElementUpdate } from '@shared/models'
-import { secondsToTime } from '../../../assets/player/utils'
 import { VideoPlaylistElement } from './video-playlist-element.model'
 import { VideoPlaylist } from './video-playlist.model'
 import { VideoPlaylistService } from './video-playlist.service'
index 55013e4c54841763ccbc751cf690a47288da4423..fcc2ce705dda5539b2c509f5bb6458060b6118a9 100644 (file)
@@ -1,5 +1,6 @@
 import { getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
 import { Actor } from '@app/shared/shared-main'
+import { buildPlaylistWatchPath } from '@shared/core-utils'
 import { peertubeTranslate } from '@shared/core-utils/i18n'
 import {
   AccountSummary,
@@ -44,7 +45,7 @@ export class VideoPlaylist implements ServerVideoPlaylist {
   videoChannelBy?: string
 
   static buildWatchUrl (playlist: Pick<VideoPlaylist, 'uuid' | 'shortUUID'>) {
-    return '/w/p/' + (playlist.shortUUID || playlist.uuid)
+    return buildPlaylistWatchPath({ shortUUID: playlist.shortUUID || playlist.uuid })
   }
 
   constructor (hash: ServerVideoPlaylist, translations: {}) {
index f1bd9f0c41d1b4f812116bbe4ed87875303a4160..2eb849d2b157f34849c35b878458a0f705c4d291 100644 (file)
@@ -2,8 +2,8 @@ import * as Hlsjs from 'hls.js/dist/hls.light.js'
 import { Events, Segment } from 'p2p-media-loader-core'
 import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
 import videojs from 'video.js'
+import { timeToInt } from '@shared/core-utils'
 import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../peertube-videojs-typings'
-import { timeToInt } from '../utils'
 import { registerConfigPlugin, registerSourceHandler } from './hls-plugin'
 
 registerConfigPlugin(videojs)
index b071a093856f0610abf8fd72844ba685b51709fa..766ad203ef7868d57f461148ab09ce3a16032c2b 100644 (file)
@@ -23,6 +23,7 @@ import './videojs-components/theater-button'
 import './playlist/playlist-plugin'
 import videojs from 'video.js'
 import { PluginsManager } from '@root-helpers/plugins-manager'
+import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
 import { isDefaultLocale } from '@shared/core-utils/i18n'
 import { VideoFile } from '@shared/models'
 import { copyToClipboard } from '../../root-helpers/utils'
@@ -33,13 +34,14 @@ import { getStoredP2PEnabled } from './peertube-player-local-storage'
 import {
   NextPreviousVideoButtonOptions,
   P2PMediaLoaderPluginOptions,
+  PeerTubeLinkButtonOptions,
   PlaylistPluginOptions,
   UserWatching,
   VideoJSCaption,
   VideoJSPluginOptions
 } from './peertube-videojs-typings'
 import { TranslationsManager } from './translations-manager'
-import { buildVideoLink, buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
+import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
 
 // Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
 (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
@@ -110,6 +112,7 @@ export interface CommonOptions extends CustomizationOptions {
   videoCaptions: VideoJSCaption[]
 
   videoUUID: string
+  videoShortUUID: string
 
   userWatching?: UserWatching
 
@@ -175,7 +178,13 @@ export class PeertubePlayerManager {
           PeertubePlayerManager.alreadyPlayed = true
         })
 
-        self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle)
+        self.addContextMenu({
+          mode,
+          player,
+          videoShortUUID: options.common.videoShortUUID,
+          videoEmbedUrl: options.common.embedUrl,
+          videoEmbedTitle: options.common.embedTitle
+        })
 
         player.bezels()
         player.stats({
@@ -218,7 +227,13 @@ export class PeertubePlayerManager {
     videojs(newVideoElement, videojsOptions, function (this: videojs.Player) {
       const player = this
 
-      self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle)
+      self.addContextMenu({
+        mode,
+        player,
+        videoShortUUID: options.common.videoShortUUID,
+        videoEmbedUrl: options.common.embedUrl,
+        videoEmbedTitle: options.common.embedTitle
+      })
 
       PeertubePlayerManager.onPlayerChange(player)
     })
@@ -295,6 +310,8 @@ export class PeertubePlayerManager {
 
       controlBar: {
         children: this.getControlBarChildren(mode, {
+          videoShortUUID: commonOptions.videoShortUUID,
+
           captions: commonOptions.captions,
           peertubeLink: commonOptions.peertubeLink,
           theaterButton: commonOptions.theaterButton,
@@ -409,6 +426,8 @@ export class PeertubePlayerManager {
   }
 
   private static getControlBarChildren (mode: PlayerMode, options: {
+    videoShortUUID: string
+
     peertubeLink: boolean
     theaterButton: boolean
     captions: boolean
@@ -497,7 +516,7 @@ export class PeertubePlayerManager {
 
     if (options.peertubeLink === true) {
       Object.assign(children, {
-        'peerTubeLinkButton': {}
+        'peerTubeLinkButton': { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions
       })
     }
 
@@ -514,7 +533,15 @@ export class PeertubePlayerManager {
     return children
   }
 
-  private static addContextMenu (mode: PlayerMode, player: videojs.Player, videoEmbedUrl: string, videoEmbedTitle: string) {
+  private static addContextMenu (options: {
+    mode: PlayerMode
+    player: videojs.Player
+    videoShortUUID: string
+    videoEmbedUrl: string
+    videoEmbedTitle: string
+  }) {
+    const { mode, player, videoEmbedTitle, videoEmbedUrl, videoShortUUID } = options
+
     const content = () => {
       const isLoopEnabled = player.options_['loop']
       const items = [
@@ -528,13 +555,15 @@ export class PeertubePlayerManager {
         {
           label: player.localize('Copy the video URL'),
           listener: function () {
-            copyToClipboard(buildVideoLink())
+            copyToClipboard(buildVideoLink({ shortUUID: videoShortUUID }))
           }
         },
         {
           label: player.localize('Copy the video URL at the current time'),
           listener: function (this: videojs.Player) {
-            copyToClipboard(buildVideoLink({ startTime: this.currentTime() }))
+            const url = buildVideoLink({ shortUUID: videoShortUUID })
+
+            copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
           }
         },
         {
index 07c7e33f6f8ff4ac004571f9e1d57da43437ffa4..919b7c2398cc0a338da3a797f07fe2063003666b 100644 (file)
@@ -1,12 +1,6 @@
-import videojs from 'video.js'
 import './videojs-components/settings-menu-button'
-import {
-  PeerTubePluginOptions,
-  ResolutionUpdateData,
-  UserWatching,
-  VideoJSCaption
-} from './peertube-videojs-typings'
-import { isMobile, timeToInt } from './utils'
+import videojs from 'video.js'
+import { timeToInt } from '@shared/core-utils'
 import {
   getStoredLastSubtitle,
   getStoredMute,
@@ -16,6 +10,8 @@ import {
   saveVideoWatchHistory,
   saveVolumeInStore
 } from './peertube-player-local-storage'
+import { PeerTubePluginOptions, ResolutionUpdateData, UserWatching, VideoJSCaption } from './peertube-videojs-typings'
+import { isMobile } from './utils'
 
 const Plugin = videojs.getPlugin('plugin')
 
index 8afb424a780c7b3602729f6d9b0fb3f0711bb1ef..d3c75990b94235f44b307a818c08106a1487cf7e 100644 (file)
@@ -132,6 +132,10 @@ type NextPreviousVideoButtonOptions = {
   isDisabled: () => boolean
 }
 
+type PeerTubeLinkButtonOptions = {
+  shortUUID: string
+}
+
 type WebtorrentPluginOptions = {
   playerElement: HTMLVideoElement
 
@@ -225,5 +229,6 @@ export {
   VideoJSPluginOptions,
   LoadedQualityData,
   QualityLevelRepresentation,
+  PeerTubeLinkButtonOptions,
   QualityLevels
 }
index 87a72b6a360758c5ad4c609ed534fd8618366449..2519a34c780fd841c10ce6b018cc40ed1eebbc2f 100644 (file)
@@ -1,7 +1,7 @@
 import videojs from 'video.js'
+import { secondsToTime } from '@shared/core-utils'
 import { VideoPlaylistElement } from '@shared/models'
 import { PlaylistItemOptions } from '../peertube-videojs-typings'
-import { secondsToTime } from '../utils'
 
 const Component = videojs.getComponent('Component')
 
index a93f595062ac98f6e4ccd5e108af2467a67fc282..b271d052660fb129039ef5c82037a2e84e7e55df 100644 (file)
@@ -1,6 +1,7 @@
 import videojs from 'video.js'
+import { secondsToTime } from '@shared/core-utils'
 import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings'
-import { bytes, secondsToTime } from '../utils'
+import { bytes } from '../utils'
 
 interface StatsCardOptions extends videojs.ComponentOptions {
   videoUUID: string
index f26176accd6a7e81af7eb4b8d30da0ab0722dd5a..f0a1b1aee246c7e5141a30a139e8560ad36d475e 100644 (file)
@@ -1,5 +1,5 @@
-import { VideoFile } from '@shared/models'
 import { escapeHTML } from '@shared/core-utils/renderer'
+import { VideoFile } from '@shared/models'
 
 function toTitleCase (str: string) {
   return str.charAt(0).toUpperCase() + str.slice(1)
@@ -43,136 +43,9 @@ function isMobile () {
   return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
 }
 
-function buildVideoLink (options: {
-  baseUrl?: string
-
-  startTime?: number
-  stopTime?: number
-
-  subtitle?: string
-
-  loop?: boolean
-  autoplay?: boolean
-  muted?: boolean
-
-  // Embed options
-  title?: boolean
-  warningTitle?: boolean
-  controls?: boolean
-  peertubeLink?: boolean
-} = {}) {
-  const { baseUrl } = options
-
-  const url = baseUrl
-    ? baseUrl
-    : window.location.origin + window.location.pathname.replace('/videos/embed/', '/w/')
-
-  const params = generateParams(window.location.search)
-
-  if (options.startTime !== undefined && options.startTime !== null) {
-    const startTimeInt = Math.floor(options.startTime)
-    params.set('start', secondsToTime(startTimeInt))
-  }
-
-  if (options.stopTime) {
-    const stopTimeInt = Math.floor(options.stopTime)
-    params.set('stop', secondsToTime(stopTimeInt))
-  }
-
-  if (options.subtitle) params.set('subtitle', options.subtitle)
-
-  if (options.loop === true) params.set('loop', '1')
-  if (options.autoplay === true) params.set('autoplay', '1')
-  if (options.muted === true) params.set('muted', '1')
-  if (options.title === false) params.set('title', '0')
-  if (options.warningTitle === false) params.set('warningTitle', '0')
-  if (options.controls === false) params.set('controls', '0')
-  if (options.peertubeLink === false) params.set('peertubeLink', '0')
-
-  return buildUrl(url, params)
-}
-
-function buildPlaylistLink (options: {
-  baseUrl?: string
-
-  playlistPosition?: number
-}) {
-  const { baseUrl } = options
-
-  const url = baseUrl
-    ? baseUrl
-    : window.location.origin + window.location.pathname.replace('/video-playlists/embed/', '/w/p/')
-
-  const params = generateParams(window.location.search)
-
-  if (options.playlistPosition) params.set('playlistPosition', '' + options.playlistPosition)
-
-  return buildUrl(url, params)
-}
-
-function buildUrl (url: string, params: URLSearchParams) {
-  let hasParams = false
-  params.forEach(() => hasParams = true)
-
-  if (hasParams) return url + '?' + params.toString()
-
-  return url
-}
-
-function generateParams (url: string) {
-  const params = new URLSearchParams(window.location.search)
-  // Unused parameters in embed
-  params.delete('videoId')
-  params.delete('resume')
-
-  return params
-}
-
-function timeToInt (time: number | string) {
-  if (!time) return 0
-  if (typeof time === 'number') return time
-
-  const reg = /^((\d+)[h:])?((\d+)[m:])?((\d+)s?)?$/
-  const matches = time.match(reg)
-
-  if (!matches) return 0
-
-  const hours = parseInt(matches[2] || '0', 10)
-  const minutes = parseInt(matches[4] || '0', 10)
-  const seconds = parseInt(matches[6] || '0', 10)
-
-  return hours * 3600 + minutes * 60 + seconds
-}
-
-function secondsToTime (seconds: number, full = false, symbol?: string) {
-  let time = ''
-
-  if (seconds === 0 && !full) return '0s'
-
-  const hourSymbol = (symbol || 'h')
-  const minuteSymbol = (symbol || 'm')
-  const secondsSymbol = full ? '' : 's'
-
-  const hours = Math.floor(seconds / 3600)
-  if (hours >= 1) time = hours + hourSymbol
-  else if (full) time = '0' + hourSymbol
-
-  seconds %= 3600
-  const minutes = Math.floor(seconds / 60)
-  if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol
-  else if (minutes >= 1) time += minutes + minuteSymbol
-  else if (full) time += '00' + minuteSymbol
-
-  seconds %= 60
-  if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol
-  else if (seconds >= 1) time += seconds + secondsSymbol
-  else if (full) time += '00'
-
-  return time
-}
-
 function buildVideoOrPlaylistEmbed (embedUrl: string, embedTitle: string) {
   const title = escapeHTML(embedTitle)
+
   return '<iframe width="560" height="315" ' +
     'sandbox="allow-same-origin allow-scripts allow-popups" ' +
     'title="' + title + '" ' +
@@ -221,11 +94,8 @@ function getRtcConfig () {
 export {
   getRtcConfig,
   toTitleCase,
-  timeToInt,
-  secondsToTime,
   isWebRTCDisabled,
-  buildPlaylistLink,
-  buildVideoLink,
+
   buildVideoOrPlaylistEmbed,
   videoFileMaxByResolution,
   videoFileMinByResolution,
index e73c95900efcde5d53d2e06e8b8ef3ec15a9ddc1..98434898cfd9f082dd48690161662f71124b89b7 100644 (file)
@@ -1,11 +1,15 @@
-import { buildVideoLink } from '../utils'
 import videojs from 'video.js'
+import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
+import { PeerTubeLinkButtonOptions } from '../peertube-videojs-typings'
 
 const Button = videojs.getComponent('Button')
 class PeerTubeLinkButton extends Button {
+  private shortUUID: string
 
-  constructor (player: videojs.Player, options?: videojs.ComponentOptions) {
-    super(player, options)
+  constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) {
+    super(player, options as any)
+
+    this.shortUUID = options.shortUUID
   }
 
   createEl () {
@@ -13,7 +17,7 @@ class PeerTubeLinkButton extends Button {
   }
 
   updateHref () {
-    this.el().setAttribute('href', buildVideoLink({ startTime: this.player().currentTime() }))
+    this.el().setAttribute('href', this.buildLink())
   }
 
   handleClick () {
@@ -22,7 +26,7 @@ class PeerTubeLinkButton extends Button {
 
   private buildElement () {
     const el = videojs.dom.createEl('a', {
-      href: buildVideoLink(),
+      href: this.buildLink(),
       innerHTML: 'PeerTube',
       title: this.player().localize('Video page (new window)'),
       className: 'vjs-peertube-link',
@@ -33,6 +37,12 @@ class PeerTubeLinkButton extends Button {
 
     return el as HTMLButtonElement
   }
+
+  private buildLink () {
+    const url = buildVideoLink({ shortUUID: this.shortUUID })
+
+    return decorateVideoLink({ url, startTime: this.player().currentTime() })
+  }
 }
 
 videojs.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton)
index b648b29e8105eb73a39484c2d0c7d61dd2239eb6..17d369c10ff1944f6db9cdd071a8fde8e1f36d38 100644 (file)
@@ -1,9 +1,7 @@
 import videojs from 'video.js'
 import * as WebTorrent from 'webtorrent'
-import { renderVideo } from './video-renderer'
-import { LoadedQualityData, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings'
-import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution, isIOS, isSafari } from '../utils'
-import { PeertubeChunkStore } from './peertube-chunk-store'
+import { timeToInt } from '@shared/core-utils'
+import { VideoFile } from '@shared/models'
 import {
   getAverageBandwidthInStore,
   getStoredMute,
@@ -11,7 +9,10 @@ import {
   getStoredVolume,
   saveAverageBandwidth
 } from '../peertube-player-local-storage'
-import { VideoFile } from '@shared/models'
+import { LoadedQualityData, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings'
+import { getRtcConfig, isIOS, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
+import { PeertubeChunkStore } from './peertube-chunk-store'
+import { renderVideo } from './video-renderer'
 
 const CacheChunkStore = require('cache-chunk-store')
 
index f7102b68ddbeb004dc8105385eb868d673f9700c..48f9a2c7cf42003d97ee22d4ee6fbaf1bc98ab23 100644 (file)
@@ -29,7 +29,7 @@
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">سجل مشاهداتي</target>
       <trans-unit id="2602586221576511475" datatype="html">
         <source>Video quota</source>
         <target>حصة الفيديو</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>غير محدود <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> في اليوم الواحد)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target state="translated">الفديرالية</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
       <trans-unit id="2454050363478003966" datatype="html">
         <source>Login</source>
         <target>لِج</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="2308975396733519902" datatype="html">
         <source>Create an account</source>
         <target>أنشئ حسابًا</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="8936704404804793618" datatype="html">
         <source>Videos</source>
         <target>الفيديوهات</target>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target>الفيديوهات</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">هذا خطأ.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">هذا الملف كبير. اتصل بالمدير حتى يزيد حد الرفع.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="2468689683507870964" datatype="html">
         <source>In this instance's network</source>
         <target state="translated">في شبكة هذ المثيل</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="4209525355702493436" datatype="html">
         <source>Ban</source>
         <target>احظر</target>
         <target state="translated">فيديو\تعليق\حساب</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">22</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target>مقبض تابع</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="3301856295120048857" datatype="html">
         <source>State <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target>حالة <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
         <target>يعرض <x id="INTERPOLATION"/> ل <x id="INTERPOLATION_1"/> من <x id="INTERPOLATION_2"/> متابع</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">المثلاء المتابَعون</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target>المضيف</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">ألغ المتابعة</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target>افتح مثيل الخادم في لسان جديد</target>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target>لم يُعثر على مضيف مطابق للمرشحات الحالية.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target>مثيل الخادم الخاص بك لا يتابع أي شخص.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target>يعرض <x id="INTERPOLATION"/> ل <x id="INTERPOLATION_1"/> من <x id="INTERPOLATION_2"/> مضيفا</target>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target>يبدو أنك لست على خادم HTTPS. يحتاج خادم الويب الخاص بك إلى تنشيط TLS لمتابعة الخوادم.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target>تابع النطاق</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
       <trans-unit id="5674286808255988565" datatype="html">
         <source>Create</source>
         <target>إنشاء</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="8147229944654164397" datatype="html">
         <source>{VAR_PLURAL, plural, =1 {Video} other {Videos} }</source>
         <target>{VAR_PLURAL, plural, =1 {فيديو} other {مقاطع فيديو} }</target>
       <trans-unit id="5248717555542428023" datatype="html">
         <source>Username</source>
         <target>اسم المستخدم</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">مثل jane_doe</target>
       <trans-unit id="4768749765465246664" datatype="html">
         <source>Email</source>
         <target>البريد الإلكتروني</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="6475711663580561164" datatype="html">
         <source>mail@example.com</source>
         <target>mail@example.com</target>
       <trans-unit id="1431416938026210429" datatype="html">
         <source>Password</source>
         <target>كلمة المرور</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8371296837649897723" datatype="html">
         <source>If you leave the password empty, an email will be sent to the user.</source>
         <target>إذا تركت كلمة المرور فارغة ، فسيتم إرسال بريد إلكتروني إلى المستخدم.</target>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target state="translated">الدور</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">تحويل الترميز مفعل. سيقتطع الحجم <x id="START_TAG_STRONG"/>الأصلي<x id="CLOSE_TAG_STRONG"/> للفيديو من حصة الفيديو.<x id="LINE_BREAK"/>يمكن لهذا المستخدم رفع ~ <x id="INTERPOLATION"/> كحد أقصى. </target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">بدون (استيثاق محلي)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target>يعرض <x id="INTERPOLATION"/> ل <x id="INTERPOLATION_1"/> من <x id="INTERPOLATION_2"/> إبلغا</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">6</context></context-group>
       </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">منصات تتابعك</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="3109314382334906782" datatype="html">
         <source>Reportee</source>
         <target>مراسل</target>
@@ -3643,9 +3641,9 @@ color: red;
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target>فيديو</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="new"> The following link contains a private token and should not be shared with anyone. </target>
@@ -3712,13 +3710,13 @@ color: red;
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">ترجمات</target>
@@ -4370,6 +4368,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">أتريد حذف <x id="PH"/>؟ ستحذف جميع فيديوهات قناة <x id="PH_1"/>، ولن تتمكن من إنشاء قناة بنفس الاسم (<x id="PH_2"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -4488,19 +4492,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="4302331889176439801" datatype="html">
         <source>Request email for account verification</source>
         <target>طلب البريد الإلكتروني للتحقق من الحساب</target>
@@ -4509,15 +4507,15 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3967269098753656610" datatype="html">
         <source>Email address</source>
         <target>عنوان البريد الإلكتروني</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4027779086550572813" datatype="html">
         <source>Send verification email</source>
         <target>إرسال رسالة التأكيد</target>
@@ -4621,11 +4619,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="6479885129995567639" datatype="html">
         <source>Video channels</source>
         <target>قنوات الفيديو</target>
@@ -5185,48 +5180,48 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Username or email address</source>
         <target>اسم المستخدم أو عنوان البريد الإلكتروني</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target>انقر هنا لإعادة تعيين كلمة المرور الخاصة بك</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target>أو قم بتسجيل الدخول باستخدام</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367" datatype="html">
         <source>Forgot your password</source>
         <target>نسيتَ كلمة المرور</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target>عذرًا ، لا يمكنك استرداد كلمة المرور الخاصة بك لأن مدير مثيل الخادم الخاص بك لم يقم بتكوين نظام البريد الإلكتروني لبيرتيوب.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -5921,8 +5916,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="new">Upload on hold</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">عذرا، عُطلت خاصية الرفع في حسابك، اتصل بمدير المنصة ليقوم بفك قَفل حصتك.</target>
@@ -6399,13 +6394,13 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6979021199788941693" datatype="html">
         <source>Your message has been sent.</source>
         <target>تم ارسال رسالتك.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target>لقد أرسلت هذا النموذج بالفعل مؤخرًا</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">فيديوهات الحساب</target>
@@ -6448,13 +6443,13 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4856575356061361269" datatype="html">
         <source><x id="PH"/> direct account followers </source>
         <target><x id="PH"/> متابعًا مباشرًا للحساب </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">بلِّغ عن هذا الحساب</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">فيديوهات</target>
@@ -6464,19 +6459,19 @@ The link will expire within 1 hour.</source>
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target>تم نسخ اسم المستخدم</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">مشترك</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> مشتركا</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target>صوت فقط</target>
@@ -6526,6 +6521,12 @@ The link will expire within 1 hour.</source>
         <source>Auto (via ffmpeg)</source>
         <target>تلقائي (عبر ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6804,17 +6805,43 @@ The link will expire within 1 hour.</source>
         <source><x id="PH"/> removed from instance followers </source>
         <target>أُزيل <x id="PH"/> من متابعي المثيل </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target>تم إرسال طلب المتابعة!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>هل تريد الغاء متابعة <x id="PH"/>؟</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target>إلغاء المتابَعة</target>
@@ -6823,8 +6850,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="3935234189109112926" datatype="html">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>انت لا تتابع <x id="PH"/> بعد الآن.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target>مفعّل</target>
@@ -7355,9 +7382,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target>خطأ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target>السجلات القياسية</target>
@@ -7398,16 +7425,8 @@ The link will expire within 1 hour.</source>
         <target>حدّث كلمة مرور المستخدم</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">قائمة المتابَعين</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">قائمة المتابِعين</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target>حُدّث حساب المستخدم <x id="PH"/>.</target>
@@ -7443,16 +7462,8 @@ The link will expire within 1 hour.</source>
         <target state="translated">الاتحادية</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">المثيلات المتابَعة</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">المثيلات المتابِعة</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">سيحذف مقاطع الفيديو ، ستحذف التعليقات.</target>
@@ -7483,6 +7494,24 @@ The link will expire within 1 hour.</source>
         <target>تعيين البريد الإلكتروني كمتحقق منه</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
@@ -7524,23 +7553,23 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target>اسم المستخدم أو كلمة المرور خاطئة.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">حسابك محجوب.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="1137937154872046253" datatype="html">
         <source>Video channel <x id="PH"/> created.</source>
         <target>أُنشئت قناة الفيديو <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target>هذا الإسم موجود على هذا المثيل.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>حُدثت قناة فيديو <x id="PH"/>.</target>
@@ -7579,11 +7608,7 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-settings.component.ts</context><context context-type="linenumber">61</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">122</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>لتأكيد اكتب الاسم العلني لقناة الفيديو <x id="PH"/></target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>حُذفت قناة فيديو <x id="PH"/>.</target>
@@ -7935,6 +7960,12 @@ The link will expire within 1 hour.</source>
         <source>Ownership change request sent.</source>
         <target>تم إرسال طلب تغيير الملكية.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -8152,7 +8183,7 @@ The link will expire within 1 hour.</source>
         <target>الاشتراك في الحساب</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="new">PLAYLISTS</target>
@@ -8438,33 +8469,33 @@ The link will expire within 1 hour.</source>
       <trans-unit id="3284171506518522275" datatype="html">
         <source>Your video was uploaded to your account and is private.</source>
         <target>تم رفع الفيديو الخاص بك إلى حسابك وهو خاص.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>ولكن ستفقد البيانات المرتبطة (العلامات ،الوصف...) ، هل تريد بالتأكيد مغادرة هذه الصفحة؟</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>لم يرفع الفيديو الخاص بك حتى الآن ، هل تريد بالتأكيد مغادرة هذه الصفحة؟</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">رفع</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target>ارفع <x id="PH"/> </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target>نُشر الفيديو.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119" datatype="html">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>لم تحفظ التغييرات! إذا غادرت ، ستفقد التغييرات.</target>
@@ -8558,27 +8589,27 @@ The link will expire within 1 hour.</source>
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">هذا الفيديو ليس متوفرا على هذا المثيل. هل تريد التوجه المثيل الأصلي: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a> ؟</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">اعادة توجيه</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>يحتوي هذا الفيديو على محتوى للبالغين أو محتوى صريح. أمتأكد من مشاهدته؟</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target>محتوى للبالغين أو محتوى صريح</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target>التالي</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">ألغ</target>
@@ -8588,62 +8619,62 @@ The link will expire within 1 hour.</source>
         <source>Autoplay is suspended</source>
         <target>أُوقف التشغيل التلقائي</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target>ادخل / اخرج من وضع ملء الشاشة (يتطلب تركيز المشغل)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target>شغل / أوقف مؤقتًا الفيديو (يتطلب تركيز المشغل)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target>أكتم / ألغ كتم الفيديو (يتطلب تركيز المشغل)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target>التخطي إلى نسبة مئوية من الفيديو: 0 هو 0٪ و 9 هو 90٪ (يتطلب تركيز المشغل)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target>زد مستوى الصوت (يتطلب تركيز اللاعب)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target>خفّض مستوى الصوت (يتطلب تركيز المشغل)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">البحث عن الفيديو (يتطلب تركيز المشغل)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">طلب الفيديو للخلف (يتطلب تركيز المشغل)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target>زيادة معدل التشغيل (يتطلب تركيز المشغل)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target>تقليل معدل التشغيل (يتطلب تركيز المشغل)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target>تصفح الفيديو إطار فإطار (يتطلب تركيز المشغل)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target>أعجبني الفيديو</target>
@@ -8769,35 +8800,35 @@ The link will expire within 1 hour.</source>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target>انتقل إلى اشتراكاتي</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target>انتقل إلى فيديوهاتي</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target>انتقل إلى مستورداتي</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target>انتقل إلى قنواتي</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target>تحتاج لإعادة الإتصال.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target>اختصارات لوحة المفاتيح:</target>
@@ -8810,6 +8841,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8838,18 +8875,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070" datatype="html">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>أكثرت المحاولات، حاول لاحقا بعد <x id="PH"/> دقيقة.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target>أكثرت المحاولات، حاول لاحقا.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target>خطأ في السيرفر. يرجى إعادة المحاولة لاحقا.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="4670312387769733978" datatype="html">
         <source>All unsaved data will be lost, are you sure you want to leave this page?</source>
         <target>ستفقد جميع البيانات غير المحفوظة ، هل تريد مغادرة هذه الصفحة؟</target>
@@ -8876,28 +8913,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target>أخف</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target>طمس</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target>اعرض</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target>مجهول</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target>أي لغة</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="9178182467454450952" datatype="html">
         <source>Confirm</source>
         <target>أكد</target>
@@ -8906,23 +8943,39 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target>اسم النطاق مطلوب.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target>أسماء النطاقات المدخلة غير صالحة.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target>تحتوي المجالات المدخلة على تكرارات.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target><x id="PH"/> غير صالح </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="7784486624424057376" datatype="html">
         <source>Instance name is required.</source>
         <target>اسم مثيل الخادم مطلوب.</target>
index 38f39ab48e806f31882b193348dbbf25552069f9..9b5195313a7c039efe045af56e7046c4dc6fe555 100644 (file)
       <trans-unit id="187187500641108332" datatype="html">
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="new">My watch history</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Crea</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">vídeo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">El següent enllaç conté un token privat i no es deuria compartir amb cap persona.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">La seua cuota de vídeo s'excedeix amb aquest vídeo (tamany del vídeo: <x id="PH" equiv-text="videoSizeBytes"/>, utilitzat: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">La seua quota diària de vídeo s'excedeix amb aquest vídeo (tamany del vídeo: <x id="PH" equiv-text="videoSizeBytes"/>, utilitzat: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">subtítols</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Quota de vídeo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="translated">Sense limit 
         <target state="translated">Federació</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Cancel·la</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="translated">Expulsa aquesta usuaria</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Aquesta instància permet el registre. No obstant aixó , tinga cura de comprovar les <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Condicions <x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> <x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Condicions <x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> abans de crear un compte. També pot buscar una altra instància que coincideixi amb les vostres necessitats: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/> <x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Usuari</target>
         <source>Username or email address</source>
         <target>Nom d'usuari o adreça de correu electrònic</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Contrasenya</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Premeu aquí per restablir la contrasenya</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">He oblidat la meua contrasenya</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Iniciar sessió amb un compte et permet publicar contingut</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Inicia sessió</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">O identifiqueu-vos amb</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Has oblidat la teva contrasenya</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">No podem recuperar la vostra contrasenya perquè l'administració de la vostra instància no ha configurat cap sistema de correus de PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Inseriu la vostra adreça de correu electrònic i us enviarem un enllaç per a restablir la contrasenya.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1161,26 +1179,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Correu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Adreça de correu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Restableix</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">en aquesta instància</target>
@@ -1531,9 +1549,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>Registrar un compte</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="new">My videos</target>
@@ -1600,10 +1618,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1682,8 +1700,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">Sóc una tetera</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Això és un error.</target>
@@ -1776,8 +1794,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">El mitjà es massa gran per al servidor. Contacta amb l'administrador si desitja augmentar la grandària límit.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">CERCA GLOBAL</target>
@@ -2482,13 +2500,13 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9172233176401579786" datatype="html">
         <source>Scheduled</source>
         <target state="translated">Programat</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-edit.component.ts</context><context context-type="linenumber">192</context></context-group></trans-unit>
+        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-edit.component.ts</context><context context-type="linenumber">192</context></context-group>
+      </trans-unit>
       <trans-unit id="1435317307066082710" datatype="html">
         <source>Hide the video until a specific date</source>
         <target state="new">Hide the video until a specific date</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-edit.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-edit.component.ts</context><context context-type="linenumber">193</context></context-group>
+      </trans-unit>
       <trans-unit id="6148369758871787018" datatype="html">
         <source>Video background image</source>
         <target state="translated">Imatge de fons del vídeo</target>
@@ -2542,8 +2560,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="new">Upload on hold</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3276,11 +3294,7 @@ The link will expire within 1 hour.</source>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Estat</target>
@@ -3355,11 +3369,7 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Amfitrió</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3371,9 +3381,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3384,13 +3394,13 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3400,16 +3410,8 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3438,9 +3440,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="8286337167859377104">
         <source>Create user</source>
         <target>Afegeix un usuari</target>
-        
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-create.component.ts</context><context context-type="linenumber">93</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.html</context><context context-type="linenumber">20</context></context-group></trans-unit>
+        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-create.component.ts</context><context context-type="linenumber">93</context></context-group>
+        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.html</context><context context-type="linenumber">20</context></context-group>
+      </trans-unit>
       <trans-unit id="8363291180171434623" datatype="html">
         <source>Table parameters</source>
         <target state="new">Table parameters</target>
@@ -3459,11 +3461,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Nom d'usuari</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3493,9 +3495,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Rol</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3520,15 +3522,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3793,6 +3789,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -4134,8 +4136,8 @@ The link will expire within 1 hour.</source>
         <target state="new">
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -6144,11 +6146,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="new">This account does not have channels.</target>
@@ -6193,6 +6192,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6782,13 +6787,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="6979021199788941693" datatype="html">
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="new">Account videos</target>
@@ -6835,13 +6840,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">
           <x id="PH"/> direct account followers
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VÍDEOS</target>
@@ -6851,31 +6856,21 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="new">Username copied</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6925,6 +6920,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -7075,18 +7076,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Il·limitat</target>
@@ -7246,26 +7263,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> no és vàlid
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Sol·licituds de seguiment enviades!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Realment vols deixar de seguir 
           <x id="PH"/>?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>No segueixis</target>
@@ -7276,8 +7319,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Ja no segueixes a 
           <x id="PH"/> .
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7768,9 +7811,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Error</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7786,8 +7829,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Usuari 
           <x id="PH"/> creat.
         </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-create.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-create.component.ts</context><context context-type="linenumber">76</context></context-group>
+      </trans-unit>
       <trans-unit id="8286337167859377104" datatype="html">
         <source>Create user</source>
         <target state="new">Create user</target>
@@ -7815,16 +7858,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7864,16 +7899,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7904,6 +7931,24 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
@@ -8217,13 +8262,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Canal de vídeo 
           <x id="PH"/> creat.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Canal de vídeo 
@@ -8246,13 +8291,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Banner deleted.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Canal de vídeo 
@@ -8418,6 +8457,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8520,7 +8565,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="new">PLAYLISTS</target>
@@ -8567,35 +8612,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Necessites tornar a connectar.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -8608,6 +8653,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8636,38 +8687,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>La contrasenya s'ha restablit correctament!</target>
@@ -10278,18 +10329,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target>Hi ha massa intents, torna-ho a provar després de 
           <x id="PH"/> minuts.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Hi ha massa intents, torna-ho a provar més tard.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Error del servidor. Torna-ho a intentar més tard.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10891,35 +10942,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275" datatype="html">
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Però es perdran les dades associades (etiquetes, descripció ...), estàs segur que vols deixar aquesta pàgina?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>El teu vídeo encara no s'ha carregat, estàs segur que vols sortir d'aquesta pàgina?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">Puja 
           <x id="PH"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Vídeo publicat.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119" datatype="html">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target state="new">You have unsaved changes! If you leave, your changes will be lost.</target>
@@ -10966,28 +11017,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Aquest vídeo conté contingut madur o explícit. Estàs segur que el vols veure?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Contingut madur o explícit</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -10996,63 +11047,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index 2bfcfb63b3fe3a449ca68fb391cc8d5421645037..61423462f709c907663b8223d6cd9b6d5887dc8d 100644 (file)
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="new">My watch history</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Vytvořit</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">Následující odkaz obsahuje soukromý token a neměl by být s nikým sdílen.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">Titulky</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Limit na videa</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Zablokovat tohoto uživatele</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Uživatel</target>
         <source>Username or email address</source>
         <target>Uživatelské jméno nebo e-mail</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Heslo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Přihlásit</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Zapomenuté heslo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1206,26 +1224,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>E-mail</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>E-mailová adresa</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="new">on this instance</target>
@@ -1593,9 +1611,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>Vytvořit účet</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="new">My videos</target>
@@ -1662,10 +1680,10 @@ The link will expire within 1 hour.</target>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1743,8 +1761,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1837,8 +1855,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2612,8 +2630,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="new">Upload on hold</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3325,11 +3343,7 @@ The link will expire within 1 hour.</target>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Stav</target>
@@ -3401,11 +3415,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Host</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3417,9 +3427,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3430,13 +3440,13 @@ The link will expire within 1 hour.</target>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3446,16 +3456,8 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3505,11 +3507,11 @@ The link will expire within 1 hour.</target>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Uživatelské jméno</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3539,9 +3541,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Role</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3566,15 +3568,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3836,6 +3832,12 @@ The link will expire within 1 hour.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -4173,8 +4175,8 @@ The link will expire within 1 hour.</target>
         <target state="new">
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -6176,11 +6178,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
         <target>Tento účet nemá žádné kanály.</target>
@@ -6225,6 +6224,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6824,12 +6829,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="new">Account videos</target>
@@ -6874,13 +6879,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">
           <x id="PH"/> direct account followers
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
@@ -6890,31 +6895,21 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="new">Username copied</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6964,6 +6959,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -7114,18 +7115,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Neomezeně</target>
@@ -7285,24 +7302,50 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> není platný
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Požadavek odeslán!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Opravdu chcete zrušit odběr kanálu <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Zrušit odběr</target>
@@ -7311,8 +7354,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>Už dále neodebíráte <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7788,9 +7831,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Chyba</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7831,16 +7874,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Uživatel <x id="PH"/> aktualizován.</target>
@@ -7878,16 +7913,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7918,6 +7945,24 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
@@ -8227,13 +8272,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1137937154872046253">
         <source>Video channel <x id="PH"/> created.</source>
         <target>Videokanál <x id="PH"/> vytvořen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Videokanál <x id="PH"/> aktualizován.</target>
@@ -8254,13 +8299,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Banner deleted.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Videokanál <x id="PH"/> odstraněn.</target>
@@ -8412,6 +8451,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8510,7 +8555,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Odebírat účet</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="new">PLAYLISTS</target>
@@ -8557,35 +8602,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Musíte se znovu připojit.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -8598,6 +8643,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8626,38 +8677,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Vaše heslo bylo úspěšně resetováno!</target>
@@ -10252,18 +10303,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>Příliš mnoho pokusů, zkuste to prosím znovu za <x id="PH"/> minut.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Příliš mnoho pokusů, zkuste to prosím později.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Chyba serveru. Zkuste to prosím později.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10858,35 +10909,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275">
         <source>Your video was uploaded to your account and is private.</source>
         <target>Vaše video bylo nahráno na váš účet a je soukromé.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Ovšem přidružená data (štítky, popis...) budou ztraceny, opravdu chcete opustit tuto stránku?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Video ještě nebylo nahráno, opravdu chcete opustit tuto stránku?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="new">Upload 
           <x id="PH"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Video publikováno</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>Máte neuložené změny! Pokud odejdete, budou vaše změny ztraceny.</target>
@@ -10954,27 +11005,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Toto video obsahuje citlivý materiál. Opravdu jej chcete přehrát?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Obsahuje citlivý materiál</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -10984,62 +11035,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>To se mi líbí</target>
index 687604dd9c81a3d744ac178d9dd8fbb935143f6f..763d1d8aad7269f1f90b5528faf2a896d8847075 100644 (file)
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Min videohistorik</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
       <trans-unit id="2602586221576511475" datatype="html">
         <source>Video quota</source>
         <target state="new">Video quota</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="new">
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="new">Ban this user</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Bruger</target>
         <source>Username or email address</source>
         <target>Brugernavn eller email-adresse</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Adgangskode</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966" datatype="html">
         <source>Login</source>
         <target state="new">Login</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Glemt din adgangskode</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1068,26 +1086,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Email</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Email-adresse</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
@@ -1460,7 +1478,7 @@ The link will expire within 1 hour.</target>
         <target>Opret en konto</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1517,7 +1535,7 @@ The link will expire within 1 hour.</target>
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1597,7 +1615,7 @@ The link will expire within 1 hour.</target>
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1681,8 +1699,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2397,7 +2415,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3125,11 +3143,7 @@ The link will expire within 1 hour.</target>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3204,11 +3218,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3221,7 +3231,7 @@ The link will expire within 1 hour.</target>
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3233,12 +3243,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3248,16 +3258,8 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3304,11 +3306,11 @@ The link will expire within 1 hour.</target>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Brugernavn</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3338,9 +3340,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target state="new">Role</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3365,15 +3367,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3632,7 +3628,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target state="new">Commented video</target>
@@ -3961,7 +3963,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5922,11 +5924,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5966,7 +5965,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6568,12 +6573,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
@@ -6623,12 +6628,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       
       <trans-unit id="1504521795586863905" datatype="html">
@@ -6643,27 +6648,19 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6713,6 +6710,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6855,18 +6858,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="new">Unlimited</target>
@@ -7026,24 +7045,50 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="new">
           <x id="PH"/> is not valid
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="translated">Do you really want to unfollow <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
@@ -7054,8 +7099,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">You are not following 
           <x id="PH"/> anymore.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7518,9 +7563,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="new">Error</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7565,16 +7610,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7614,16 +7651,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7654,7 +7683,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -7964,12 +8011,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> created.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="new">Video channel 
@@ -7986,13 +8033,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="new">Video channel 
@@ -8151,6 +8192,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8254,7 +8301,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -8300,35 +8347,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -8339,6 +8386,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8362,38 +8415,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="new">Your password has been successfully reset!</target>
@@ -9977,18 +10030,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target state="new">Too many attempts, please try again after 
           <x id="PH"/> minutes.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10582,35 +10635,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target>Din video blev uploadet til din konto og er privat.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Din video er ikke uploadet endnu, er du sikker på, at du vil forlade denne side?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="new">Upload 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Video offentliggjort.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10678,27 +10731,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -10708,62 +10761,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index 6f2d33ba579dfffe3503a710d2b98624d77bd10d..0a8ca956b0d2cae483d72c655ed937cd73e02c91 100644 (file)
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Mein Verlauf</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Erstellen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">Video</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">Der folgende Link enthält ein privates Token und sollte nicht an Dritte weitergegeben werden.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">Ihr Videokontingent ist mit diesem Video überschritten (Videogröße: <x id="PH" equiv-text="videoSizeBytes"/>, verwendet: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, Kontingent: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">Ihr tägliches Videokontingent wurde mit diesem Video überschritten (Videogröße: <x id="PH" equiv-text="videoSizeBytes"/>, verwendet: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, Kontingent: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">Untertitel</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Videokontingent</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Unbegrenzt <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> pro Tag)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target state="translated">Föderation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Diesen Nutzer sperren</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Diese Instanz erlaubt eine Registrierung. Lesen sie sich trotzdem die <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Richtlinien<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Richtlinien<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>durch, bevor sie sich ein Konto erstellen. Hier können sie auch nach einer anderen Instanz suchen, die ihren Wünschen entspricht: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Diese Instanz erlaubt zurzeit keine Registrierung, sie können sich die <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Richtlinien<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> für mehr Details durchlesen, oder hier nach einer Instanz suchen, die Ihnen das Anlegen eines Kontos und das Hochladen von Videos erlaubt: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Benutzer</target>
         <source>Username or email address</source>
         <target>Benutzername oder E-Mail-Adresse</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Passwort</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Klicken Sie hier, um Ihr Passwort zurückzusetzen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Ich habe mein Passwort vergessen</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Durch das Anmelden können sie Inhalte hochladen</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Anmelden</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Oder anmelden mit</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Passwort vergessen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Es tut uns Leid, du kannst dein Kennwort nicht wiederherstellen weil dein Instanzadministrator das PeerTube-E-Mail-System nicht konfiguriert hat.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1098,26 +1116,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>E-Mail</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>E-Mail-Adresse</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Zurücksetzen</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">auf dieser Instanz</target>
@@ -1444,9 +1462,9 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>Konto erstellen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">Meine Videos</target>
@@ -1513,10 +1531,10 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDEOS</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">Gleichzeitigkeit von Importaufträgen</target>
@@ -1594,8 +1612,8 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">Ich bin ein Teekessel</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Dies ist ein Fehler.</target>
@@ -1688,8 +1706,8 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">Das Medium ist zu groß für den Server. Bitte wenden Sie sich an Ihren Administrator, wenn Sie das Größenlimit erhöhen möchten.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2430,8 +2448,8 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="translated">Upload angehalten</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Entschuldigung, Ihr Account kann keine Videos hochladen. Wenn Sie Videos hochladen möchten, muss ein Administrator Ihr Videokontingent freischalten.</target>
@@ -3129,11 +3147,7 @@ Hilf mit PeerTube zu übersetzen!</target>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target>Folger-Identifikator</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Status</target>
@@ -3199,11 +3213,7 @@ Hilf mit PeerTube zu übersetzen!</target>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action }}"/> </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Host</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Redundanz erlaubt <x id="START_TAG_P-SORTICON"/> <x id="CLOSE_TAG_P-SORTICON"/></target>
@@ -3212,9 +3222,9 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Nichtmehr folgen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Öffne die Instanz in einem neuen Tab</target>
@@ -3225,28 +3235,20 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Kein Host für die aktuellen Filter gefunden.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Ihre Instanz folgt niemandem.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Zeige <x id="INTERPOLATION"/> bis <x id="INTERPOLATION_1"/> von <x id="INTERPOLATION_2"/> Ho</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Folge Domains</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Instanzen folgen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">Aktion</target>
@@ -3296,11 +3298,11 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Nutzername</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">z.B. max_mustermann</target>
@@ -3328,9 +3330,9 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Benutzerrolle</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Transkodierung aktiviert. Das Videokontingent wird anhand der <x id="START_TAG_STRONG"/>originalen<x id="CLOSE_TAG_STRONG"/> Videogröße berechnet. <x id="LINE_BREAK"/> Dieser Nutzer kann maximal ~ <x id="INTERPOLATION"/> hochladen. </target>
@@ -3347,15 +3349,9 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Auth-Plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Keine (lokale Authentifizierung)</target>
@@ -3606,6 +3602,12 @@ Hilf mit PeerTube zu übersetzen!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3906,8 +3908,8 @@ Hilf mit PeerTube zu übersetzen!</target>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Es sieht so aus, dass Ihr Server kein HTTPS verwendet. Auf Ihrem Webserver muss TLS aktiviert sein, damit Sie Servern folgen können.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Domains stummschalten</target>
@@ -5856,11 +5858,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">KANÄLE</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
         <target>Dieses Konto hat keine Kanäle.</target>
@@ -5899,6 +5898,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Wollen Sie wirklich <x id="PH" equiv-text="videoChannel.displayName"/> löschen? Es löscht <x id="PH_1" equiv-text="videoChannel.videosCount"/> Videos, die in diesem Kanal hochgeladen wurden, und Sie können keinen weiteren Kanal mit demselben Namen (<x id="PH_2" equiv-text="videoChannel.name"/>) erstellen!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6415,12 +6420,12 @@ Erstelle mein Konto</target>
         <source>Your message has been sent.</source>
         <target>Deine Nachricht wurde gesendet.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Du hast dieses Formular bereits kürzlich gesendet</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Videos des Kontos</target>
@@ -6463,13 +6468,13 @@ Erstelle mein Konto</target>
       <trans-unit id="4856575356061361269" datatype="html">
         <source><x id="PH"/> direct account followers </source>
         <target state="translated"><x id="PH"/> direkte Kontofolgende </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Dieses Konto melden</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDEOS</target>
@@ -6479,31 +6484,21 @@ Erstelle mein Konto</target>
       <trans-unit id="25349740244798533">
         <source>Username copied</source>
         <target>Benutzername kopiert</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 Abonnent</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> Abonnenten</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Gefolgte Instanzen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Folgende Instanzen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Nur Ton</target>
@@ -6553,6 +6548,12 @@ Erstelle mein Konto</target>
         <source>Auto (via ffmpeg)</source>
         <target>Automatisch (über ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6701,18 +6702,34 @@ Erstelle mein Konto</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Domain wird benötigt.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Eingegebene Domains sind ungültig.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Eingegebene Domains enthalten Wiederholungen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Unbegrenzt</target>
@@ -6872,24 +6889,50 @@ Erstelle mein Konto</target>
           <x id="PH"/> von den Instanzfolgern entfernt
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> ist ungültig
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Anfrage(n) zum Folgen gesendet!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Möchtest du <x id="PH"/> wirklich nicht mehr folgen?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Nicht mehr folgen</target>
@@ -6898,8 +6941,8 @@ Erstelle mein Konto</target>
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>Du folgst <x id="PH"/> nicht mehr.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>aktiviert</target>
@@ -7371,9 +7414,9 @@ Erstelle mein Konto</target>
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Fehler</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Standard Log-Dateien</target>
@@ -7414,16 +7457,8 @@ Erstelle mein Konto</target>
         <target>Benutzerkennwort geändert</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Liste aller Gefolgten</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Liste aller Follower</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Benutzer <x id="PH"/> aktualisiert.</target>
@@ -7459,16 +7494,8 @@ Erstelle mein Konto</target>
         <target state="translated">Föderation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Gefolgte Instanzen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Folgende Instanzen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Videos werden gelöscht, Kommentare werden mit einem Grabstein markiert.</target>
@@ -7499,6 +7526,24 @@ Erstelle mein Konto</target>
         <target>E-Mail als bestätigt setzen</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
@@ -7803,13 +7848,13 @@ Erstelle mein Konto</target>
       <trans-unit id="1137937154872046253">
         <source>Video channel <x id="PH"/> created.</source>
         <target>Videokanal <x id="PH"/> erstellt.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Dieser Name existiert bereits auf dieser Instanz.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Videokanal <x id="PH"/> aktualisiert.</target>
@@ -7830,11 +7875,7 @@ Erstelle mein Konto</target>
         <target state="translated">Banner gelöscht.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Bitte gib zum Bestätigen den Anzeigenamen des Videokanals ( <x id="PH"/>) ein</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Videokanal <x id="PH"/> entfernt.</target>
@@ -7986,6 +8027,12 @@ Erstelle mein Konto</target>
         <source>Ownership change request sent.</source>
         <target>Eine Anfrage zur Änderung des Besitzers wurde versendet.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8084,7 +8131,7 @@ Erstelle mein Konto</target>
         <target>Diesen Account abonnieren</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">PLAYLISTS</target>
@@ -8131,34 +8178,34 @@ Erstelle mein Konto</target>
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Gehe zu meinen Abos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Zu meinen Videos gehen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Gehe zu meinen Importen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Gehe zu meinen Kanälen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Referenzen des OAuth-Clients können nicht abgerufen werden: <x id="PH" equiv-text="error.text"/>. Stellen Sie sicher, dass PeerTube korrekt konfiguriert ist (Ordner config/), speziell der Abschnitt "webserver".</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Bitte verbinde dich erneut.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Tastaturkürzel:</target>
@@ -8171,6 +8218,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8199,38 +8252,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>Falscher Benutzername oder falsches Passwort.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Ihr Konto wurde gesperrt.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">jede Sprache</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">verstecken</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">verwischen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">anzeigen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Unbekannt</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Dein Passwort wurde zurückgesetzt!</target>
@@ -9810,18 +9863,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>Zu viele Versuche in kurzer Zeit. Bitte versuche es in <x id="PH"/> Minuten nochmal.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Zu viele Versuche in kurzer Zeit. Bitte versuche es später nochmal.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Server-Fehler. Bitte später erneut versuchen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Alle Kanäle von <x id="PH"/> abonniert. Du wirst über neue Videos darin benachrichtigt.</target>
@@ -10410,35 +10463,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275">
         <source>Your video was uploaded to your account and is private.</source>
         <target>Das Video wurde in dein Konto hochgeladen und ist privat.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Weitere Infos (Tags, Beschreibung, ...) werden verworfen, wenn du diese Seite verlässt. Bist du dir sicher?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Dein Video ist noch nicht hochgeladen. Willst du diese Seite wirklich verlassen?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Hochladen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload <x id="PH"/> </source>
         <target>
           <x id="PH"/> hochladen
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Video veröffentlicht.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>Es gibt ungespeicherte Änderungen! Wenn du die Seite verlässt, gehen die Änderungen verloren.</target>
@@ -10486,27 +10539,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Dieses Video ist auf dieser Instanz nicht verfügbar. Wollen Sie auf die Quellinstanz weitergeleitet werden: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Weiterleitung</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Dieses Video enthält Inhalte, die möglicherweise für bestimmte Zuschauer ungeeignet sind oder von diesen als anstößig empfunden werden. Möchtest du es wirklich ansehen?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Inhalt, der möglicherweise für bestimmte Zuschauer ungeeignet oder anstößig ist</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Nächstes</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Abbrechen</target>
@@ -10516,62 +10569,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="translated">Autoplay ist unterbrochen</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Vollbildmodus betreten/verlassen (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Video abspielen/pausieren (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Video stummschalten/Stummschaltung aufheben (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Springe zu einem Prozentsatz des Videos: 0 entspricht 0%, 9 entspricht 90% (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Lautstärke erhöhen (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Lautstärke verringern (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Vorspulen (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Zurückspulen (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Abspielgeschwindigkeit erhöhen (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Abspielgeschwindigkeit verringern (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Einen Frame weitergehen (Player benötigt Fokus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Das Video gefällt mir</target>
index a3b37b853f890093aaf3c2ed09ad443a5bb08dbb..a3fb28d387915d7d39f15834dd7a1f530293f8d5 100644 (file)
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Το ιστορικό μου</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">βίντεο</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">Ο σύνδεσμος που ακολουθεί περιέχει απόρρητη συμβολοσειρά και δεν θα πρέπει να τον μοιραστείτε με τρίτους.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">υπότιτλοι</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Όριο βίντεο</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="translated">ακόλουθοι</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Αποκλεισμός χρήστη</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Χρήστης</target>
         <source>Username or email address</source>
         <target>Όνομα χρήστη ή διεύθυνση e-mail</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Κωδικός σύνδεσης</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Ξέχασα τον κωδικό μου</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Σύνδεση</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Ή συνδεθείτε με:</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Ξεχάσατε τον κωδικό σύνδεσης</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1053,26 +1071,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>E-mail</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Διεύθυνση e-mail</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Επαναφορά</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="new">on this instance</target>
@@ -1440,7 +1458,7 @@ The link will expire within 1 hour.</target>
         <target>Δημιουργία λογαριασμού</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1497,7 +1515,7 @@ The link will expire within 1 hour.</target>
         <source>VIDEOS</source>
         <target state="translated">ΒΙΝΤΕΟ</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1576,7 +1594,7 @@ The link will expire within 1 hour.</target>
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Αυτό είναι ένα σφάλμα.</target>
@@ -1669,8 +1687,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2404,7 +2422,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3150,11 +3168,7 @@ The link will expire within 1 hour.</target>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target>Ψευδώνυμο (handle) ακολούθου</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Κατάσταση</target>
@@ -3229,11 +3243,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Κόμβος</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3246,7 +3256,7 @@ The link will expire within 1 hour.</target>
         <source>Unfollow</source>
         <target state="translated">Αφαίρεση ακολούθησης</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3258,12 +3268,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3273,16 +3283,8 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3331,11 +3333,11 @@ The link will expire within 1 hour.</target>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Όνομα χρήστη</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3365,9 +3367,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Ρόλος</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3392,15 +3394,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3660,7 +3656,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target state="new">Commented video</target>
@@ -3987,7 +3989,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5985,11 +5987,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
         <target>Αυτός ο λογαριασμός δεν έχει κανάλια.</target>
@@ -6032,7 +6031,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6630,12 +6635,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target>Το μήνυμά σας έχει σταλεί.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Στείλατε ήδη αυτή τη φόρμα πρόσφατα</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="new">Account videos</target>
@@ -6681,13 +6686,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">
           <x id="PH"/> direct account followers
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">ΒΙΝΤΕΟ</target>
@@ -6697,29 +6702,21 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="25349740244798533">
         <source>Username copied</source>
         <target>Το όνομα χρήστη αντιγράφτηκε</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6769,6 +6766,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target>Αυτόματα (με ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6911,18 +6914,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Απεριόριστα</target>
@@ -7082,26 +7101,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> διαγράφηκε από ακόλουθος του κόμβου
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> δεν είναι έγκυρο
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Τα αιτήματα ακολούθησης στάλθηκαν!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Θέλετε πραγματικά να σταματήσετε να ακολουθείτε το 
           <x id="PH"/>;
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Αφαίρεση ακολούθησης</target>
@@ -7112,8 +7157,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Δεν ακολουθείτε το 
           <x id="PH"/> πια.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>ενεργοποιήθηκε</target>
@@ -7596,9 +7641,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Σφάλμα</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7643,16 +7688,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Ενημέρωση κωδικού χρήστη</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7692,16 +7729,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7732,7 +7761,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Το e-mail έχει επιβεβαιωθεί</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
         <target>Δεν μπορείτε να αποκλείστε τον root.</target>
@@ -8047,13 +8094,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Δημιουργήθηκε το κανάλι 
           <x id="PH"/>.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Το όνομα υπάρχει ήδη σ' αυτόν τον κόμβο</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Το κανάλι 
@@ -8076,13 +8123,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Παρακαλούμε πληκτρολογήστε το όνομα καναλιού του (
-          <x id="PH"/>) για επιβεβαίωση
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Το κανάλι 
@@ -8241,6 +8282,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target>Το αίτημα αλλαγής κατόχου έχει σταλεί.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8345,7 +8392,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Συνδρομή στον λογαριασμό</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="new">PLAYLISTS</target>
@@ -8392,35 +8439,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Μετάβαση στις συνδρομές μου</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Μετάβαση στα βίντεό μου</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Μετάβαση στις εισαγωγές μου</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Μετάβαση στα κανάλια μου</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Πρέπει να ξανασυνδεθείτε.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Συντομεύσεις πληκτρολογίου:</target>
@@ -8431,6 +8478,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8453,39 +8506,39 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>Λάθος όνομα χρήστη ή κωδικός.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Άγνωστο</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Ο κωδικός σας έχει ανανεωθεί με επιτυχία!</target>
@@ -10079,18 +10132,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target>Πάρα πολλές προσπάθειες, δοκιμάστε ξανά μετά από 
           <x id="PH"/> λεπτά.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Πάρα πολλές προσπάθειες, δοκιμάστε ξανά αργότερα.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Σφάλμα κόμβου. Δοκιμάστε ξανά αργότερα.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10686,35 +10739,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target>Το βίντεο ανέβηκε στον λογαριασμό σας και είναι ιδιωτικό.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Όμως οι σχετικές πληροφορίες (ετικέτες, περιγραφή...) θα χαθούν, σίγουρα θέλετε να φύγετε από τη σελίδα;</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Το βίντεο δεν έχει ανέβει ακόμα, θέλετε σίγουρα να φύγετε από τη σελίδα;</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Αποστολή</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload <x id="PH"/> </source>
         <target>Ανεβάστε 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Το βίνεο δημοσιεύτηκε.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119">
@@ -10764,27 +10817,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Το βίντεο έχει σκληρό περιεχόμενο ή μόνο για ενήλικες. Σίγουρα θέλετε να το δείτε;</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Σκληρό περιεχόμενο ή για ενήλικες</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Επόμενο</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Ακύρωση</target>
@@ -10794,62 +10847,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Μειώστε το ποσοστό αναπαραγωγής (απαιτεί εστίαση του προγράμματος αναπαραγωγής))</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Περιήγηση στο βίντεο καρέ ανά καρέ (απαιτεί εστίαση του προγράμματος αναπαραγωγής)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Σας αρέσει το βίντεο</target>
index 034488b82c941f0389ec0b84d065491e85ab2c2f..b7b8cdb7adb5a47e1cdfe51622ae491d78350d55 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source><target state="new">My watch history</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source><target state="new">subtitles</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source> Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Ban this user</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html</context><context context-type="linenumber">16</context></context-group></trans-unit><trans-unit id="7252854992688790751" datatype="html">
         <source> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit><trans-unit id="7215649348148521605" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7215649348148521605" datatype="html">
         <source> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>User</target>
         <source>Username or email address</source>
         <target>Username or email address</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
+      </trans-unit>
       
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source><target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit><trans-unit id="2101170466365500913" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="2101170466365500913" datatype="html">
         <source> Logging into an account lets you publish content </source><target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Login</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Forgot your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
         <source> Enter your email address and we will send you a link to reset your password. </source><target state="new"> Enter your email address and we will send you a link to reset your password. </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</source><target state="new">An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</target>
@@ -1139,17 +1157,17 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Email address</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source><target state="new">Reset</target>
         
         <note priority="1" from="description">Password reset button</note>
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       
       <trans-unit id="4319634264526091601" datatype="html">
@@ -1511,7 +1529,7 @@ The link will expire within 1 hour.</target>
         <source>Create an account</source>
         <target>Create an account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source><target state="new">My videos</target>
@@ -1557,7 +1575,7 @@ The link will expire within 1 hour.</target>
         <target state="new">VIDEOS</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="new">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1625,7 +1643,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/menu/notification.component.html</context><context context-type="linenumber">49</context></context-group></trans-unit><trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source><target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source><target state="new">That's an error.</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context>
@@ -1691,7 +1709,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context><context context-type="linenumber">42</context></context-group></trans-unit><trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source><target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2385,7 +2403,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3045,11 +3063,7 @@ The link will expire within 1 hour.</target>
         <target>ID</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group></trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>State</target>
@@ -3131,11 +3145,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3146,7 +3156,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source><target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3158,12 +3168,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3173,14 +3183,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit><trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source><target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3230,7 +3233,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source><target state="new">e.g. jane_doe</target>
         
         <note priority="1" from="description">Username choice placeholder in the registration form</note>
@@ -3262,7 +3265,7 @@ The link will expire within 1 hour.</target>
         <target>Role</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source> Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3285,15 +3288,9 @@ The link will expire within 1 hour.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="new">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3520,7 +3517,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="4691552465058437520" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit><trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source><target state="new">Commented video</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">82</context></context-group></trans-unit><trans-unit id="7266085473379376028" datatype="html">
@@ -3842,7 +3845,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5647,11 +5650,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5689,7 +5689,13 @@ channel with the same name (<x id="PH_2"/>)!</source><target state="new">Do you
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6283,12 +6289,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target>Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source><target state="new">Account videos</target>
         
@@ -6323,10 +6329,10 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source><target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source><target state="new">VIDEOS</target>
@@ -6338,24 +6344,16 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source><target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source><target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6403,7 +6401,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target>Auto (via ffmpeg)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="931255636742351800" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit><trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source><target state="new">No limit</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="5250062810079582285" datatype="html">
@@ -6522,17 +6526,33 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group></trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
+      </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Unlimited</target>
@@ -6663,7 +6683,27 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="2740793005745065895">
         <source>
           <x id="PH"/> is not valid
@@ -6672,19 +6712,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> is not valid
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Follow request(s) sent!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Do you really want to unfollow 
           <x id="PH"/>?
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Unfollow</target>
@@ -6696,7 +6742,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> anymore.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>enabled</target>
@@ -7154,7 +7200,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7192,13 +7238,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Update user password</source>
         <target>Update the user password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit><trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source><target state="new">Following list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group></trans-unit><trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source><target state="new">Followers list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit>
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7229,13 +7269,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/users.routes.ts</context><context context-type="linenumber">45</context></context-group></trans-unit><trans-unit id="8564701209009684429" datatype="html">
         <source>Federation</source><target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source><target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group></trans-unit><trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source><target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7263,7 +7297,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
         <target>You cannot ban root.</target>
@@ -7568,12 +7620,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> created.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Video channel 
@@ -7590,13 +7642,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Video channel 
@@ -7739,7 +7785,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target>Ownership change request sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
         <target>My channels</target>
@@ -7834,7 +7886,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7881,34 +7933,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Go to my subscriptions</source>
         <target>Go to my subscriptions</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Go to my videos</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Go to my imports</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Go to my channels</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source><target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       
       
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>You need to reconnect.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Keyboard Shortcuts:</target>
@@ -7919,6 +7971,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -7942,38 +8000,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target>Incorrect username or password.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Your password has been successfully reset!</target>
@@ -9517,17 +9575,17 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/> minutes.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Too many attempts, please try again later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Server error. Please retry later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10025,20 +10083,20 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target>Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source><target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload 
           <x id="PH"/>
@@ -10047,13 +10105,13 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Video published.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119">
@@ -10116,26 +10174,26 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source><target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source><target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source><target state="new">Cancel</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">646</context></context-group></trans-unit>
@@ -10143,62 +10201,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Like the video</target>
index 707ab9e3b53d9f7cc234ffcdca4e51ec0327b69a..33c475f963c9883d74a9019cd7addf109d53a15f 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source><target state="final">My watch history</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="final">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="final"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="final">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="final">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source><target state="final">subtitles</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source> Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="final"> Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <source>Federation</source>
         <target state="final">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="final">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="final">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="final">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="final">Ban this user</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html</context><context context-type="linenumber">16</context></context-group></trans-unit><trans-unit id="7252854992688790751" datatype="html">
         <source> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="final"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit><trans-unit id="7215649348148521605" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7215649348148521605" datatype="html">
         <source> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="final"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729" datatype="html">
         <source>User</source>
         <target state="final">User</target>
         <source>Username or email address</source>
         <target state="final">Username or email address</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="final"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
+      </trans-unit>
       
       <trans-unit id="1431416938026210429" datatype="html">
         <source>Password</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="final">Click here to reset your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source><target state="final">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit><trans-unit id="2101170466365500913" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="2101170466365500913" datatype="html">
         <source> Logging into an account lets you publish content </source><target state="final"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966" datatype="html">
         <source>Login</source>
         <target state="final">Login</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="final">Or sign in with</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367" datatype="html">
         <source>Forgot your password</source>
         <target state="final">Forgot your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="final">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
         <source> Enter your email address and we will send you a link to reset your password. </source><target state="final"> Enter your email address and we will send you a link to reset your password. </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</source><target state="final">An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</target>
@@ -991,17 +1009,17 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610" datatype="html">
         <source>Email address</source>
         <target state="final">Email address</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source><target state="final">Reset</target>
         
         <note priority="1" from="description">Password reset button</note>
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       
       <trans-unit id="4319634264526091601" datatype="html">
@@ -1323,7 +1341,7 @@ The link will expire within 1 hour.</target>
         <source>Create an account</source>
         <target state="final">Create an account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source><target state="final">My videos</target>
@@ -1369,7 +1387,7 @@ The link will expire within 1 hour.</target>
         <target state="final">VIDEOS</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="final">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1437,7 +1455,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/menu/notification.component.html</context><context context-type="linenumber">49</context></context-group></trans-unit><trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source><target state="final">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source><target state="final">That's an error.</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context>
@@ -1503,7 +1521,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context><context context-type="linenumber">42</context></context-group></trans-unit><trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source><target state="final">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2181,7 +2199,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="final">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="final">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -2826,11 +2844,7 @@ The link will expire within 1 hour.</target>
         <target state="final">ID</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group></trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="final">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="final">State</target>
@@ -2905,11 +2919,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="final">Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="final">Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -2917,7 +2927,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source><target state="final">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="final">Open instance in a new tab</target>
@@ -2929,25 +2939,18 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="final">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="final">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="final">Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="final">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit><trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source><target state="final">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="final">Action</target>
         
         
@@ -2997,7 +3000,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source><target state="final">e.g. jane_doe</target>
         
         <note priority="1" from="description">Username choice placeholder in the registration form</note>
@@ -3029,7 +3032,7 @@ The link will expire within 1 hour.</target>
         <target state="final">Role</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source> Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="final"> Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </target>
@@ -3044,15 +3047,9 @@ The link will expire within 1 hour.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="final">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="final">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3269,7 +3266,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="4691552465058437520" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="final">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit><trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source><target state="final">Commented video</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">82</context></context-group></trans-unit><trans-unit id="7266085473379376028" datatype="html">
@@ -3555,7 +3558,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="final">Mute domains</target>
@@ -5312,11 +5315,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="final">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5350,7 +5350,13 @@ channel with the same name (<x id="PH_2"/>)!</source><target state="final">Do yo
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="final">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="final">My Channels</target>
@@ -5901,12 +5907,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="final">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="final">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source><target state="final">Account videos</target>
         
@@ -5939,10 +5945,10 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source><target state="final">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source><target state="final">VIDEOS</target>
@@ -5954,24 +5960,16 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="final">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source><target state="final">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source><target state="final"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="final">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="final">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="final">Audio-only</target>
@@ -6019,7 +6017,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="final">Auto (via ffmpeg)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="931255636742351800" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="final">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit><trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source><target state="final">No limit</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="5250062810079582285" datatype="html">
@@ -6138,17 +6142,33 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Domain is required.</source>
         <target state="final">Domain is required.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group></trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="final">Domains entered are invalid.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="final">Domains entered contain duplicates.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="final">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="final">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="final">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="final">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
+      </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="final">Unlimited</target>
@@ -6279,7 +6299,27 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="final">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="final">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source>
           <x id="PH"/> is not valid
@@ -6288,17 +6328,23 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> is not valid
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="final">Follow request(s) sent!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="final">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="final">Do you really want to unfollow <x id="PH"/>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="final">Unfollow</target>
@@ -6308,7 +6354,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>You are not following <x id="PH"/> anymore.</source>
         <target state="final">You are not following <x id="PH"/> anymore.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="final">enabled</target>
@@ -6747,7 +6793,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="final">Standard logs</target>
@@ -6781,13 +6827,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Update user password</source>
         <target state="final">Update user password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit><trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source><target state="final">Following list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group></trans-unit><trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source><target state="final">Followers list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit>
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="final">User <x id="PH"/> updated.</target>
@@ -6814,13 +6854,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/users.routes.ts</context><context context-type="linenumber">45</context></context-group></trans-unit><trans-unit id="8564701209009684429" datatype="html">
         <source>Federation</source><target state="final">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source><target state="final">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group></trans-unit><trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source><target state="final">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="final">Videos will be deleted, comments will be tombstoned.</target>
@@ -6848,7 +6882,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="final">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="final">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="final">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="final">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="final">You cannot ban root.</target>
@@ -7145,12 +7197,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Video channel <x id="PH"/> created.</source>
         <target state="final">Video channel <x id="PH"/> created.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="final">This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="final">Video channel <x id="PH"/> updated.</target>
@@ -7165,11 +7217,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="final">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="final">Please type the display name of the video channel (<x id="PH"/>) to confirm</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="final">Video channel <x id="PH"/> deleted.</target>
@@ -7298,7 +7346,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="final">Ownership change request sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="final">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
         <target state="final">My channels</target>
@@ -7389,7 +7443,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="final">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="final">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7436,34 +7490,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Go to my subscriptions</source>
         <target state="final">Go to my subscriptions</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="final">Go to my videos</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="final">Go to my imports</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="final">Go to my channels</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source><target state="final">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       
       
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="final">You need to reconnect.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="final">Keyboard Shortcuts:</target>
@@ -7474,6 +7528,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="final">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="final">Trending</target>
         
@@ -7497,38 +7557,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target state="final">Incorrect username or password.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="final">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="final">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="final">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="final">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="final">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="final">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="final">Your password has been successfully reset!</target>
@@ -9048,17 +9108,17 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target state="final">Too many attempts, please try again after <x id="PH"/> minutes.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="final">Too many attempts, please try again later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="final">Server error. Please retry later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="final">Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</target>
@@ -9549,20 +9609,20 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="final">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="final">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="final">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source><target state="final">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload 
           <x id="PH"/>
@@ -9571,13 +9631,13 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="final">Video published.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -9640,26 +9700,26 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source><target state="final">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source><target state="final">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="final">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="final">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="final">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source><target state="final">Cancel</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">646</context></context-group></trans-unit>
@@ -9667,62 +9727,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="final">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="final">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="final">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="final">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="final">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="final">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="final">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="final">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="final">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="final">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="final">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="final">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="final">Like the video</target>
index 8847790944304ae989e36c3c2074cda581ec7ec4..e18655e10bafb0de083bf379daa28bc029ddc957 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Historio de mia spektado</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">filmo</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Datumlimo por filmoj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Senlima <x id="START_TAG_NG_CONTAINER"/>(po <x id="INTERPOLATION"/> tage) <x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <source>Federation</source>
         <target>Federaĵo</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106">
         <source>followers</source>
         <target>abonantoj</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Forbari ĉi tiun uzanton</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Ĉi tiu nodo permesas registriĝojn. Tamen zorge tralegu <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>la uzokondiĉojn<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>uzokondiĉojn<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> antaŭ ol vi kreos konton. Eble vi ankaŭ povus trovi nodon, kiu precize konvenos viajn bezonojn, per: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Ĉi tiu nodo nun ne permesas registriĝon de uzantoj. Vi povas malkovri pliajn detalojn per <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>la uzokondiĉoj<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> aŭ trovi nodon, kiu ebligas registradon de kontoj kaj alŝutadon de filmoj. Trovu la vian inter pluraj nodoj per: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Uzanto</target>
         <source>Username or email address</source>
         <target>Salutnomo aŭ retpoŝtadreso</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Pasvorto</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Klaku ĉi tien por restarigi vian pasvorton</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Mi forgesis pasvorton</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Salutinte vian konton, vi povas publikigi enhavon</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Saluti</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Aŭ saluti per</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Forgesita pasvorto</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target>
       Ni pardonpetas; vi ne povas rehavi vian pasvorton, ĉar la administranto de via nodo ne agordis la retpoŝtan sistemon de PeerTube.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Enigu vian retpoŝtadreson kaj ni sendos al vi retleteron por restarigi vian pasvorton.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1017,26 +1035,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Retpoŝtadreso</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Retpoŝtadreso</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Restarigi</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
@@ -1365,7 +1383,7 @@ The link will expire within 1 hour.</source>
         <target>Krei konton</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1422,7 +1440,7 @@ The link will expire within 1 hour.</source>
         <source>VIDEOS</source>
         <target state="translated">FILMOJ</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1502,7 +1520,7 @@ The link will expire within 1 hour.</source>
         <source>I'm a teapot</source>
         <target state="translated">Mi estas tekruĉo</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Tio estas eraro.</target>
@@ -1586,8 +1604,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">La vidaŭdaĵo estas tro granda por la servilo. Bonvolu kontakti vian administranton, se vi volas pligrandigi la datumlimon.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2267,7 +2285,7 @@ The link will expire within 1 hour.</source>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Pardonu, la alŝuta funkcio estas malŝaltita por via konto. Se vi volas aldoni filmojn, administranto devas malŝlosi vian datumlimon.</target>
@@ -2977,11 +2995,7 @@ The link will expire within 1 hour.</source>
         <target>Identigilo</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target>Nomo de abonanto</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Stato</target>
@@ -3049,11 +3063,7 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Gastiganto</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Ripetaĵo permesita <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3063,7 +3073,7 @@ The link will expire within 1 hour.</source>
         <source>Unfollow</source>
         <target state="translated">Malaboni</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Malfermi nodon en nova langeto</target>
@@ -3075,27 +3085,19 @@ The link will expire within 1 hour.</source>
         <source>No host found matching current filters.</source>
         <target state="translated">Neniu gastiganto troviĝis per la nunaj filtriloj.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Via nodo neniun abonas.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Montrante <x id="INTERPOLATION"/> ĝis <x id="INTERPOLATION_1"/> el <x id="INTERPOLATION_2"/> gastigantoj</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Aboni domajnojn</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Aboni nodojn</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3142,11 +3144,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Salutnomo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">ekz. ushio_shiromija</target>
@@ -3174,9 +3176,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Rolo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Transkodado estas ŝaltita. Ĉi tiu datumlimo konsideras <x id="START_TAG_STRONG"/>originalan<x id="CLOSE_TAG_STRONG"/> grandecon de la filmo. <x id="LINE_BREAK"/> Plej grande ĉi tiu uzanto povus alŝuti ~ <x id="INTERPOLATION"/>. </target>
@@ -3193,15 +3195,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Aŭtentikiga kromprogramo</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Neniu (loka aŭtentikigo)</target>
@@ -3446,7 +3442,13 @@ The link will expire within 1 hour.</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target state="translated">Komentita filmo</target>
@@ -3733,7 +3735,7 @@ The link will expire within 1 hour.</source>
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Ŝajnas, ke vi ne uzas servilon kun HTTPS. Via retservilo bezonas aktivan protokolon TLS por aboni servilojn.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Silentigi domajnojn</target>
@@ -5632,11 +5634,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
@@ -5670,7 +5669,13 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Ĉu vi certe volas forigi <x id="PH"/>? Tio forigos ĉiujn filmojn alŝutitajn de <x id="PH_1"/> al ĉi tiu kanalo, kaj vi ne povos krei alian kanalon kun la sama nomo (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="translated">Miaj kanaloj</target>
@@ -6191,12 +6196,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Your message has been sent.</source>
         <target>Via mesaĝo sendiĝis.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Vi jam sendis ĉi tiun respondilon freŝdate</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
@@ -6244,12 +6249,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <x id="PH"/> rektaj abonantoj de konto
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Raporti ĉi tiun konton</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       
       <trans-unit id="1504521795586863905" datatype="html">
@@ -6264,27 +6269,19 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Salutnomo kopiiĝis</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 abonanto</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> abonantoj</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Nodoj, kiujn vi abonas</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Nodoj, kiuj vin abonas</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Nur sono</target>
@@ -6334,6 +6331,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target>Memage (per ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6474,18 +6477,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Domajno estas postulata.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Enigitaj domajnoj estas nevalidaj.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Enigitaj domajnoj enhavas duoblaĵojn.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Senlima</target>
@@ -6645,24 +6664,50 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <x id="PH"/> forigita el abonantoj de nodo
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> ne validas
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Abonpetoj senditaj!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Ĉu vi certe volas malaboni <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Malaboni</target>
@@ -6671,8 +6716,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>Vi ne plu abonas <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>ŝaltita</target>
@@ -7120,9 +7165,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Eraro</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Normaj protokoloj</target>
@@ -7163,16 +7208,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Ĝisdatigi pasvorton de uzanto</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Listo de abonatoj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Listo de abonantoj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Uzanto <x id="PH"/> ĝisdatigita.</target>
@@ -7208,16 +7245,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Federaĵo</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Abonataj nodoj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Abonantaj nodoj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Filmoj foriĝos, komentoj tombiĝos.</target>
@@ -7248,7 +7277,25 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Agordi retpoŝtadreson konfirmita</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
         <target>Vi ne povas forbari ĉefuzanton.</target>
@@ -7551,12 +7598,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Video channel <x id="PH"/> created.</source>
         <target>Filma kanalo <x id="PH"/> kreita.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Ĉi tiu nomo jam ekzistas ĉe ĉi tiu nodo.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Filma kanalo <x id="PH"/> ĝisdatigita.</target>
@@ -7571,11 +7618,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Bonvolu entajpi la prezentan nomon de la filma kanalo (<x id="PH"/>) por konfirmi</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Filma kanalo <x id="PH"/> forigita.</target>
@@ -7724,6 +7767,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target>Peto de poseda ŝanĝo sendiĝis.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -7825,7 +7874,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Aboni la konton</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7871,34 +7920,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Iri al miaj abonoj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Iri al miaj filmoj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Iri al miaj enportoj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Iri al miaj kanaloj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Ne povas ekhavi salutilojn de OAuth Client: <x id="PH"/>. Certigu, ke PeerTube estas ĝuste agordita (en la dosierujo config/), precipe la sekcio «webserver».</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Vi devas rekonektiĝi.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Fulmoklavoj:</target>
@@ -7909,6 +7958,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -7932,38 +7987,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target>Malĝusta salutnomo aŭ pasvorto.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Via konto estas blokita.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">ajna lingvo</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">kaŝi</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">malklarigi</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">montri</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Nekonata</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Via pasvorto estas sukcese restarigita!</target>
@@ -9524,18 +9579,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>Tro multaj petoj; bonvolu reprovi post <x id="PH"/> minutoj.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Tro multaj provoj; bonvolu reprovi poste.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Servila eraro. Bonvolu reprovi poste.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Abonanta ĉiujn nunajn kanalojn de <x id="PH"/>. Vi sciiĝos pri ĉiuj ĝiaj novaj filmoj.</target>
@@ -10120,35 +10175,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target>Via filmo alŝutiĝis al via konto kaj estas privata.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Sed rilataj informoj (etikedoj, priskribo…) perdiĝos; ĉu vi certe volas folasi ĉi tiun paĝon?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Via filmo ankoraŭ ne alŝutiĝis; ĉu vi certe volas forlasi ĉi tiun paĝon?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Alŝuti</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload <x id="PH"/> </source>
         <target>Alŝuti 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Filmo publikigita.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119">
@@ -10196,27 +10251,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Ĉi tiu filmo ne estas disponebla per ĉi tiu nodo. Ĉu vi volas alidirektiĝi al la devena nodo: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Alidirektiĝo</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Tiu ĉi video povas esti konsterna aŭ maltaŭga por neplenaĝuloj. Ĉu vi certe volas spekti ĝin?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Konsterna aŭ maltaŭga por neplenaĝaj</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Sekve</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Nuligi</target>
@@ -10226,62 +10281,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="translated">Memludado estas haltigita</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Eniĝi/eliĝi tutekranan reĝimon (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Ludi/Paŭzgi la filmon (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Silentigi/Malsilentigi la filmon (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Salti al elcenta progreso en la filmo: 0 estas 0% kaj 9 estas 90% (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Plilaŭtigi (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Mallaŭtigi (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Pluigi la filmon (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Malpluigi la filmon (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Plirapidigi la filmon (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Malrapidigi la filmon (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Navigi tra la filmo po filmeroj (bezonas fokusitan ludilon)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Ŝati la filmon</target>
index d2905bb7b3156c71e72594dd039312215ca3e762..62fe3b7041cfcf5d83d9ea2c2ea268c00a0d8ab5 100644 (file)
         <target state="translated">
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Mi historial de visionados</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Crear</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">video</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">El siguiente enlace contiene un token privado y no debe compartirse con nadie.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">Su cuota de video se excede con este video (tamaño del video:<x id="PH" equiv-text="videoSizeBytes"/>, usado: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">Su cuota diaria de video se excede con este video (tamaño del video:<x id="PH" equiv-text="videoSizeBytes"/>, usado: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">subtítulos</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Cupo de vídeos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Illimitado <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> por día) <x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target state="translated">Federación</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
       <trans-unit id="2159130950882492111">
         <source>Cancel</source>
         <target>cancelar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Expulsar este usuario</target>
@@ -1005,19 +1029,13 @@ Iniciar sesión</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Esta instancia permite el registro. Sin embargo, tenga cuidado de comprobar las <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Condiciones<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Condiciones<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>antes de crear una cuenta. También puede buscar otra instancia que coincida con sus necesidades exactas en: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Actualmente, esta instancia no permite el registro de usuarios, puede marcar la <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>para obtener más detalles o busque una instancia que le brinde la posibilidad de registrarse para obtener una cuenta y cargar sus videos allí. Encuentre el suyo entre varias instancias en:<x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Usuario</target>
@@ -1028,67 +1046,67 @@ Iniciar sesión</target>
         <source>Username or email address</source>
         <target>Nombre de usuario o correo electrónico</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Contraseña</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Haga clic aquí para restablecer la contraseña</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Olvidé mi contraseña</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Iniciar sesión en una cuenta le permite publicar contenido</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Iniciar sesión</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">O inicia sesión con</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Contraseña olvidada</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Lo sentimos, no es posible recuperar la contraseña porque el administrador de la instancia no ha configurado el sistema de correo electrónico de PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Ingrese su dirección de correo electrónico y le enviaremos un enlace para restablecer su contraseña.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1098,26 +1116,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Correo electrónico </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Correo electrónico </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Reiniciar</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">en esta instancia</target>
@@ -1445,9 +1463,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>Crear una cuenta</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">Mis videos</target>
@@ -1514,10 +1532,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VÍDEOS</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">Importar simultaneidad de trabajos</target>
@@ -1596,8 +1614,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">Soy una tetera</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Eso es un error.</target>
@@ -1690,8 +1708,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">El medio es demasiado grande para el servidor. Comuníquese con su administrador si desea aumentar el tamaño del límite.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">BÚSQUEDA GLOBAL</target>
@@ -2429,8 +2447,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="translated">Subir en espera</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Lo sentimos, la función de carga está deshabilitada para su cuenta. Si desea agregar videos, un administrador debe desbloquear su cuota.</target>
@@ -3113,11 +3131,7 @@ The link will expire within 1 hour.</source>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="translated">Control de seguidor</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Estado</target>
@@ -3185,11 +3199,7 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Host</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Redundancia permitida <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3198,9 +3208,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Dejar de seguir</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Abrir instancia en una pestaña nueva</target>
@@ -3211,28 +3221,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">No se ha encontrado ningún host que coincida con los filtros actuales.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Tu instancia no sigue a nadie.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Mostrando <x id="INTERPOLATION"/> a <x id="INTERPOLATION_1"/> de <x id="INTERPOLATION_2"/> hosts</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Seguir dominios</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Seguir instancias</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">Acción</target>
@@ -3282,11 +3284,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Nombre de usuario</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">ejemplo: jane_doe</target>
@@ -3314,9 +3316,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Rol</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">La transcodificación está habilitada. La cuota de video solo tiene en cuenta el peso <x id="START_TAG_STRONG"/>original <x id="CLOSE_TAG_STRONG"/> del vídeo. <x id="LINE_BREAK"/> Como máximo, este usuario podría subir ~ <x id="INTERPOLATION"/>. </target>
@@ -3333,15 +3335,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Complemento de autenticación</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Ninguna autenticación local</target>
@@ -3592,6 +3588,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3891,8 +3893,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Parece que no estás en un servidor HTTPS. Su servidor web necesita tener TLS activado para seguir otros servidores.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Dominios silenciados</target>
@@ -5842,11 +5844,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">CANALES</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">Esta cuenta no tiene canales.</target>
@@ -5885,6 +5884,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">¿Realmente quieres eliminar <x id="PH" equiv-text="videoChannel.displayName"/>? Se eliminarán<x id="PH_1" equiv-text="videoChannel.videosCount"/>videos subidos en este canal ¡y no podrás crear otro canal con el mismo nombre (<x id="PH_2" equiv-text="videoChannel.name"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6398,13 +6403,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="6979021199788941693">
         <source>Your message has been sent.</source>
         <target>Su mensaje ha sido enviado.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Ya envió este formulario recientemente</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Videos de cuenta</target>
@@ -6449,13 +6454,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">
           <x id="PH"/> seguidores de cuenta directa
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Informar sbre esta cuenta</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VÍDEOS</target>
@@ -6465,31 +6470,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">Nombre de usuario copiado</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 suscriptor</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> suscriptores</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Instancias que sigues</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Instancias que te siguen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Solo audio</target>
@@ -6539,6 +6534,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target>Auto (vía ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6689,18 +6690,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Se requiere dominio.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Los dominios ingresados no son válidos.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Los dominios ingresados contienen duplicados.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Ilimitado</target>
@@ -6870,24 +6887,50 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <x id="PH"/> eliminado de seguidores de instancia
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> no es válido
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>¡Petición(es) de seguimiento enviada(s)!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>¿De verdad quieres dejar de seguir a <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Dejar de seguir</target>
@@ -6896,8 +6939,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>Ya no estás siguiendo a <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>habilitada</target>
@@ -7369,9 +7412,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Error</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Registros estándar</target>
@@ -7412,16 +7455,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Actualizar contraseña de usuario</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Lista de seguidores</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Lista de seguidores</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Usuario <x id="PH"/> actualizado.</target>
@@ -7457,16 +7492,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Federación</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Instancias que sigues</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Instancias siguiéndote</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Los videos serán eliminados, los comentarios serán destruidos.</target>
@@ -7497,6 +7524,24 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Establecer la dirección de correo electrónico como Verificada</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
@@ -7801,13 +7846,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1137937154872046253">
         <source>Video channel <x id="PH"/> created.</source>
         <target>Canal de vídeo <x id="PH"/> creado.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>El nombre ya existe en esta instancia.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Canal de vídeo <x id="PH"/> actualizado.</target>
@@ -7828,11 +7873,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Banner eliminado.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="translated">Escriba el nombre para mostrar del canal de video ( <x id="PH"/>) para confirmar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Canal de vídeo <x id="PH"/> eliminado.</target>
@@ -7984,6 +8025,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target>Solicitud de cambio de titularidad enviada.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8082,7 +8129,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Suscribirse a la cuenta</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">LISTAS DE REPRODUCCIÓN</target>
@@ -8129,34 +8176,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Ir a mis suscripciones</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Ir a mis vídeos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Ir a mis importaciones</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Ir a mis canales</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">No se pueden recuperar las credenciales del cliente OAuth: <x id="PH" equiv-text="error.text"/>. Asegúrese de haber configurado correctamente PeerTube (config / directorio), en particular la sección "servidor web".</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Tienes que reconectar.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Atajos de teclado:</target>
@@ -8169,6 +8216,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8197,38 +8250,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>El nombre de usuario o la contraseña son incorrectos.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Tu cuenta está bloqueada.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">cualquier idioma</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">esconder</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">difuminar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">monitor</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Desconocido</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>¡Tu contraseña ha sido restablecida con éxito!</target>
@@ -9814,18 +9867,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>Demasiados intentos, por favor inténtalo de nuevo pasados <x id="PH"/> minutos.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Demasiados intentos, por favor inténtelo más tarde.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Error del servidor. Por favor, inténtalo más tarde.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Suscrito a todos los canales actuales de <x id="PH"/>. Serás notificado de todos sus nuevos videos.</target>
@@ -10408,35 +10461,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275">
         <source>Your video was uploaded to your account and is private.</source>
         <target>Tu vídeo ha sido subida a tu cuenta y es privado.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Pero los datos asociados (etiquetas, descripción...) se perderán, ¿seguro que quieres abandonar esta página?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Tu vídeo aún no se ha subido, ¿seguro que quieres abandonar esta página?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Subir</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">Cargue 
           <x id="PH"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Vídeo publicado.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119" datatype="html">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target state="translated">¡Usted tiene cambios no guardados! Si te vas, tus cambios se perderán.</target>
@@ -10483,28 +10536,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Este video no está disponible en esta instancia. ¿Quieres ser redirigido a la instancia de origen: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Redirección</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Este vídeo contiene material para adultos o explícito. ¿Seguro que lo quieres ver?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Contenido para adultos o explícito</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Hasta la siguiente</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Cancelar</target>
@@ -10513,63 +10566,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">La reproducción automática está suspendida</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Entrar/salir de pantalla completa (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Reproducir / Pausar el video (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Silenciar / activar el video (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Salte a un porcentaje del video: 0 es 0% y 9 es 90% (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Aumenta el volumen (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Disminuye el volumen (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Buscar el video hacia adelante (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Busque el video hacia atrás (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Aumentar la velocidad de reproducción (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Disminuir la velocidad de reproducción (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Navegue en el video cuadro por cuadro (requiere foco en el reproductor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Colocar Me gusta a este vídeo</target>
index 627f6c5d37d0d2e41bc573dc943147787540c579..d3486f1bcc2f99dafd386232d4f0073234883b81 100644 (file)
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Nire ikustaldien erregistroa</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">bideoa</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Bideo-kuota</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>
         <source>Federation</source>
         <target state="translated">Federazioa</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="translated">jarraitzaile</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Debekatu erabiltzaile hau</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Erabiltzailea</target>
         <source>Username or email address</source>
         <target>Erabiltzaile-izena edo eposta helbidea</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Pasahitza</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Egin klik hemen zure pasahitza berrezartzeko</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Pasahitza ahaztu dut</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Kontu batekin sartzeak edukia argitaratzea ahalbidetuko dizu</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Hasi saioa</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Edo hasi saioa honekin</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Pasahitza ahaztu duzu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Sentitzen dugu, ezin duzu zure pasahitza berreskuratu, instantziaren administratzaileak ez baitzuen PeerTubeko posta elektronikoko sistema konfiguratu.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Sartu zure eposta helbidea eta zure pasahitza berrezartzeko esteka bat bidaliko dizugu.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1080,26 +1098,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Eposta</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Eposta helbidea</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Berrezarri</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
@@ -1438,7 +1456,7 @@ The link will expire within 1 hour.</source>
         <target>Sortu kontu bat</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1495,7 +1513,7 @@ The link will expire within 1 hour.</source>
         <source>VIDEOS</source>
         <target state="translated">BIDEOAK</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1575,7 +1593,7 @@ The link will expire within 1 hour.</source>
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1659,8 +1677,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">Multimedia-fitxategia handiegia da zerbitzarirako. Kontaktatu administratzailea tamaina-muga handitu nahi baduzu.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2346,7 +2364,7 @@ The link will expire within 1 hour.</source>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Igotzeko-funtzioa desgaituta dago zure konturako. Bideoak gehitu nahi badituzu, administratzaile batek zure kuota desblokeatu behar du.</target>
@@ -3070,11 +3088,7 @@ The link will expire within 1 hour.</source>
         <target>ID-a</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Egoera</target>
@@ -3149,11 +3163,7 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Ostalaria </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3166,7 +3176,7 @@ The link will expire within 1 hour.</source>
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Zabaldu instantzia fitxa berri batean</target>
@@ -3178,12 +3188,12 @@ The link will expire within 1 hour.</source>
         <source>No host found matching current filters.</source>
         <target state="translated">Ez da aurkitu iragazkiekin bat datorren ostalaririk.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Zure instantziak ez du besterik jarraitzen</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3193,16 +3203,8 @@ The link will expire within 1 hour.</source>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Jarraitu domeinuak</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3249,11 +3251,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Erabiltzaile izena</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3281,9 +3283,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Rola</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3308,15 +3310,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3575,7 +3571,13 @@ The link will expire within 1 hour.</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target state="new">Commented video</target>
@@ -3902,7 +3904,7 @@ The link will expire within 1 hour.</source>
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Badirudi ez zaudela HTTPS zerbitzari batean. Zure web zerbitzariak TLS aktibatuta eduki behar du zerbitzariak jarraitu ahal izateko.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Mututu domeinuak</target>
@@ -5838,11 +5840,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5882,7 +5881,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="translated">Nire kanalak</target>
@@ -6475,12 +6480,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
@@ -6530,12 +6535,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       
       <trans-unit id="1504521795586863905" datatype="html">
@@ -6550,27 +6555,19 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6620,6 +6617,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target>Automatikoa (ffmpeg bidez)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6762,18 +6765,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Mugagabea</target>
@@ -6933,26 +6952,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> baliogabea da
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Jarraitzeko eskaria(k) bidalita!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Ziur
           <x id="PH"/> jarraitzeari utzi nahi diozula?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Utzi jarraitzeari</target>
@@ -6963,8 +7008,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Ez duzu jada 
           <x id="PH"/> jarraitzen.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>gaituta</target>
@@ -7431,9 +7476,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Errorea</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7478,16 +7523,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7527,16 +7564,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7567,7 +7596,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Jo eposta egiaztatutzat</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
         <target>Ezin duzu root debekatu</target>
@@ -7877,12 +7924,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> bideo kanala sortuta.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Izen hau hartuta dago instantzia honetan</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>
@@ -7899,13 +7946,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>
@@ -8068,6 +8109,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target>Jabetza aldatzeko eskaria bidalita.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8171,7 +8218,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Harpidetu kontura</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -8217,35 +8264,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Joan nire harpidetzetara</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Joan nire bideoetara</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Joan nire inportazioetara</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Joan nire kanaletara</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Berriro konektatu behar duzu.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Teklatu laster-bideak:</target>
@@ -8256,6 +8303,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8279,38 +8332,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target>Erabiltzaile-izen edo pasahitz okerra.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Zure pasahitza ongi berrezarri da!</target>
@@ -9897,18 +9950,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target>Saiakera gehiegi, saiatu berriro geroago, 
           <x id="PH"/> minutu igarotakoan.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Saiakera gehiegi, saiatu berriro geroago.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Zerbitzariaren errorea, Saiatu berriro geroago.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10502,35 +10555,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target>Zure bideoa zure kontura igo da eta pribatua da.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Baina dagozkion datuak (etiketak, deskripzioa...) galduko dira, ziur orri hau utzi nahi duzula?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Zur bideoa ez da igo oraindik, ziur orri hau utzi nahi duzula?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="new">Upload 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Bideoa argitaratuta.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10580,27 +10633,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Bideo honek helduentzako edo hunkigarria den edukia du. Ziur ikusi nahi duzula?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Helduentzako edo hunkigarria den edukia</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -10610,62 +10663,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Gehitu bideoa gogokoetara</target>
index 5d8ef8d28eeab84d4fb21943d5b5858df12888e0..35f589b3063e2fd13d036f280686bbbf82bd3432 100644 (file)
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="new">My watch history</target>
       <trans-unit id="5674286808255988565" datatype="html">
         <source>Create</source>
         <target state="new">Create</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">پیوند زیر دارای یک رمز خصوصی است و نباید با کسی به اشتراک گذاشته شود.</target>
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">با این ویدیو از سهمیه ویدیوی شما بیشتر می شود (اندازه ویدیو: <x id="PH" equiv-text="videoSizeBytes"/>، استفاده شده: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>، سهم : <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">سهمیه ویدئویی روزانه شما با این ویدیو (اندازه ویدیو: <x id="PH" equiv-text="videoSizeBytes"/>استفاده شده: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>، سهم: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">زیرنویس</target>
       <trans-unit id="2602586221576511475" datatype="html">
         <source>Video quota</source>
         <target state="new">Video quota</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="new">
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="new">Ban this user</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>کاربر</target>
         <source>Username or email address</source>
         <target>نام کاربری یا آدرس رایانامه</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>گذرواژه</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>ورود</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>گذرواژه‌تان را فراموش کرده‌اید</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1113,26 +1131,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>رایانامه</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>آدرس رایانامه</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="new">on this instance</target>
@@ -1508,7 +1526,7 @@ The link will expire within 1 hour.</target>
         <target>ساخت حساب</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1576,7 +1594,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1654,8 +1672,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1748,8 +1766,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2523,7 +2541,7 @@ The link will expire within 1 hour.</target>
         <source>Upload on hold</source>
         <target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3239,11 +3257,7 @@ The link will expire within 1 hour.</target>
         <target state="new">ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target state="new">State</target>
@@ -3318,11 +3332,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>میزبان</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3334,9 +3344,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3347,13 +3357,13 @@ The link will expire within 1 hour.</target>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3363,16 +3373,8 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3422,11 +3424,11 @@ The link will expire within 1 hour.</target>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target state="new">Username</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3456,9 +3458,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>نقش</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3483,15 +3485,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3756,6 +3752,12 @@ The link will expire within 1 hour.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -4096,8 +4098,8 @@ The link will expire within 1 hour.</target>
         <target state="new">
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -6101,11 +6103,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="new">This account does not have channels.</target>
@@ -6148,6 +6147,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6746,12 +6751,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="new">Account videos</target>
@@ -6797,13 +6802,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">
           <x id="PH"/> direct account followers
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
@@ -6813,31 +6818,21 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="new">Username copied</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6887,6 +6882,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -7037,18 +7038,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="new">Unlimited</target>
@@ -7208,26 +7225,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="new">
           <x id="PH"/> is not valid
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
@@ -7238,8 +7281,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">You are not following 
           <x id="PH"/> anymore.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7730,9 +7773,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="new">Error</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7777,16 +7820,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7826,16 +7861,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7866,7 +7893,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -8179,13 +8224,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Video channel 
           <x id="PH"/> created.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="new">Video channel 
@@ -8208,13 +8253,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="new">Video channel 
@@ -8380,6 +8419,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8482,7 +8527,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="new">PLAYLISTS</target>
@@ -8529,35 +8574,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -8568,6 +8613,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8594,39 +8645,39 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="new">Your password has been successfully reset!</target>
@@ -10231,18 +10282,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target state="new">Too many attempts, please try again after 
           <x id="PH"/> minutes.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10841,34 +10892,34 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="new">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="new">Upload 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>ویدئو انتشار‌یافت</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119" datatype="html">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target state="new">You have unsaved changes! If you leave, your changes will be lost.</target>
@@ -10936,27 +10987,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -10966,62 +11017,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index 574375b5f3371ee977b65d8a3fc96e160ef4d47f..dd127aea827f3c17b92fd916f9923b07bf310c3c 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source><target state="new">My watch history</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source><target state="new">subtitles</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source> Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Sulje tämä käyttäjä</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html</context><context context-type="linenumber">16</context></context-group></trans-unit><trans-unit id="7252854992688790751" datatype="html">
         <source> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit><trans-unit id="7215649348148521605" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7215649348148521605" datatype="html">
         <source> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Käyttäjä</target>
         <source>Username or email address</source>
         <target>Käyttäjänimi tai sähköpostiosoite</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
+      </trans-unit>
       
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source><target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit><trans-unit id="2101170466365500913" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="2101170466365500913" datatype="html">
         <source> Logging into an account lets you publish content </source><target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Kirjaudu sisään</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Unohda salasanasi</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
         <source> Enter your email address and we will send you a link to reset your password. </source><target state="new"> Enter your email address and we will send you a link to reset your password. </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</source><target state="new">An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</target>
@@ -1138,17 +1156,17 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Sähköpostiosoite</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source><target state="new">Reset</target>
         
         <note priority="1" from="description">Password reset button</note>
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       
       <trans-unit id="4319634264526091601" datatype="html">
@@ -1488,7 +1506,7 @@ The link will expire within 1 hour.</target>
         <source>Create an account</source>
         <target>Luo tili</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source><target state="new">My videos</target>
@@ -1534,7 +1552,7 @@ The link will expire within 1 hour.</target>
         <target state="new">VIDEOS</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="new">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1602,7 +1620,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/menu/notification.component.html</context><context context-type="linenumber">49</context></context-group></trans-unit><trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source><target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source><target state="new">That's an error.</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context>
@@ -1668,7 +1686,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context><context context-type="linenumber">42</context></context-group></trans-unit><trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source><target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2358,7 +2376,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3026,11 +3044,7 @@ The link will expire within 1 hour.</target>
         <target>ID</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group></trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target>Seuraajan käsittelijä</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Tila</target>
@@ -3112,11 +3126,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Isäntä</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3127,7 +3137,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source><target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3139,12 +3149,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3154,14 +3164,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit><trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source><target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3211,7 +3214,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source><target state="new">e.g. jane_doe</target>
         
         <note priority="1" from="description">Username choice placeholder in the registration form</note>
@@ -3243,7 +3246,7 @@ The link will expire within 1 hour.</target>
         <target>Rooli</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source> Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3266,15 +3269,9 @@ The link will expire within 1 hour.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="new">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3501,7 +3498,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="4691552465058437520" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit><trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source><target state="new">Commented video</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">82</context></context-group></trans-unit><trans-unit id="7266085473379376028" datatype="html">
@@ -3823,7 +3826,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5622,11 +5625,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
@@ -5664,7 +5664,13 @@ channel with the same name (<x id="PH_2"/>)!</source><target state="new">Do you
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6263,12 +6269,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target>Viestisi on lähetetty.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Lähetit jo tämän lomakkeen vasta.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source><target state="new">Account videos</target>
         
@@ -6303,10 +6309,10 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source><target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source><target state="new">VIDEOS</target>
@@ -6318,24 +6324,16 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Käyttäjänimi kopioitu</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source><target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source><target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6383,7 +6381,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target>Automaattinen (ffmpeg avulla)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="931255636742351800" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit><trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source><target state="new">No limit</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="5250062810079582285" datatype="html">
@@ -6502,17 +6506,33 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group></trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
+      </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Rajaton</target>
@@ -6643,7 +6663,27 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="2740793005745065895">
         <source>
           <x id="PH"/> is not valid
@@ -6652,19 +6692,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> ei ole sallittu
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Seurantapyynnöt lähetetty!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Lopeta seuranta</target>
@@ -6676,7 +6722,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> enään.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>käytössä</target>
@@ -7134,7 +7180,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7172,13 +7218,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Update user password</source>
         <target>Päivitä tilin salasana</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit><trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source><target state="new">Following list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group></trans-unit><trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source><target state="new">Followers list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit>
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7209,13 +7249,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/users.routes.ts</context><context context-type="linenumber">45</context></context-group></trans-unit><trans-unit id="8564701209009684429" datatype="html">
         <source>Federation</source><target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source><target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group></trans-unit><trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source><target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7243,7 +7277,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Aseta sähköpostiosoite vahvistetuksi</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
         <target>Et voi estää root-käyttäjää.</target>
@@ -7548,12 +7600,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> luotiin.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Tämä nimi on jo olemassa tässä instanssissa.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Videokanava 
@@ -7570,13 +7622,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="new">Video channel 
@@ -7719,7 +7765,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target>Omistajuudenvaihtopyyntö lähetetty.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
         <target>Minun kanavat</target>
@@ -7814,7 +7866,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Tilaa käyttäjä</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7861,34 +7913,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Go to my subscriptions</source>
         <target>Mene tilauksiini</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Mene videoihini</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Mene tuonteihini</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Mene kanaviini</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source><target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       
       
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Sinun pitää yhdistää udelleen.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Pikanäppäimet:</target>
@@ -7899,6 +7951,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -7922,38 +7980,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target>Virheellinen käyttäjänimi tai salasana.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="new">Your password has been successfully reset!</target>
@@ -9497,17 +9555,17 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/> minutes.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Liian monta yritystä, yritä myöhemmin uudelleen.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Palvelinvirhe. Yritä myöhemmin uudelleen.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10005,20 +10063,20 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Videota ei ole vielä ladattu, haluatko varmasti poistua sivulta?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source><target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload 
           <x id="PH"/>
@@ -10027,13 +10085,13 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Video julkaistu.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119">
@@ -10096,26 +10154,26 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source><target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source><target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Tämä video sisältää aikuisille tarkoitettua sisältöä. Haluatko varmasti jatkaa?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Aikuisille tarkoitettu sisältö</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source><target state="new">Cancel</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">646</context></context-group></trans-unit>
@@ -10123,62 +10181,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Tykkää videosta</target>
index a092c252fa389f5fa65dd7b341dba18a598387e7..2a64674e756ba4e132e1b359c7e6e18cc3f11801 100644 (file)
         <target state="translated">
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Mon historique de visionnage</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Créer</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">vidéo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">Le lien suivant contient un jeton privé et ne doit être partagé avec personne.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">Votre quota est dépassé avec cette vidéo (taille de la vidéo : <x id="PH" equiv-text="videoSizeBytes"/>, utilisé : <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota : <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">Votre quota journalier est dépassé avec cette vidéo (taille de la vidéo : <x id="PH" equiv-text="videoSizeBytes"/>, utilisé : <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota : <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">sous-titres</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Quota des vidéos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Illimité <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> par jour)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target>Fédération</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106">
         <source>followers</source>
       <trans-unit id="2159130950882492111">
         <source>Cancel</source>
         <target>Annuler</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Bannir cet·te utilisateur·rice</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Cette instance permet l'enregistrement. Toutefois, il faut veiller à vérifier les <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>conditions d'utilisation<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>conditions d'utilisation<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> avant de créer un compte. Vous pouvez également rechercher une autre instance correspondant exactement à vos besoins sur : <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Actuellement, cette instance ne permet pas l'enregistrement des utilisateurs, vous pouvez vérifier les <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>conditions d'utilisation<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> pour plus de détails ou trouvez une instance qui vous donne la possibilité de créer un compte et d'y télécharger vos vidéos. Trouvez la vôtre parmi plusieurs instances à l'adresse suivante : <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Utilisateur·rice</target>
         <source>Username or email address</source>
         <target>Identifiant ou adresse de courriel</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Mot de passe</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Cliquez ici pour réinitialiser votre mot de passe</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">J'ai oublié mon mot de passe</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">La connexion à un compte vous permet de publier du contenu</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Se connecter</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Ou connectez vous</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Oubli de votre mot de passe</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target>Nous sommes désolés, vous ne pouvez pas réinitialiser votre mot de passe car l'administrateur·rice de votre instance n'a pas configuré le système de courrier électronique de PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Saisissez votre adresse électronique et nous vous enverrons un lien pour réinitialiser votre mot de passe.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1096,26 +1114,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Courriel</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Adresse de courriel</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Réinitialiser</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">sur cette instance</target>
@@ -1445,9 +1463,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>Créer un compte</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">Mes vidéos</target>
@@ -1514,10 +1532,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDÉOS</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">Importer des travaux en même temps</target>
@@ -1596,8 +1614,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">Je suis une théière</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">C'est une erreur.</target>
@@ -1690,8 +1708,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">Ce média est trop gros pour le serveur. Merci de contacter votre administrateur pour augmenter cette limite.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">RECHERCHE GLOBALE</target>
@@ -2433,8 +2451,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="translated">Téléversement en attente</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Désolé, la fonction de téléchargement est désactivée pour votre compte. Si vous souhaitez ajouter des vidéos, un administrateur doit débloquer votre quota.</target>
@@ -3120,11 +3138,7 @@ The link will expire within 1 hour.</source>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target>Identifiant d'abonné·e</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Statut</target>
@@ -3192,11 +3206,7 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Hôte</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Redondance autorisée <x id="START_TAG_P-SORTICON"/><x id="CLOSE_TAG_P-SORTICON"/></target>
@@ -3205,9 +3215,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Arrêter de suivre</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Ouvrir l'instance dans une nouvelle fenêtre</target>
@@ -3218,28 +3228,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Impossible de trouver un hôte correspondant aux critères actuels.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Votre instance n'en suit aucune autre.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Affiche <x id="INTERPOLATION"/> à <x id="INTERPOLATION_1"/> sur <x id="INTERPOLATION_2"/>hôtes</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Suivre des domaines</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Suivre les instances</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">Action</target>
@@ -3289,11 +3291,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Identifiant</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">exemple : joel_dove</target>
@@ -3321,9 +3323,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Rôle</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Le transcodage est activé. Le quota de vidéos ne prend en compte que la taille du fichier <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/>.<x id="LINE_BREAK"/> L'utilisateur peut au plus téléverser ~ <x id="INTERPOLATION"/>. </target>
@@ -3340,15 +3342,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Plugin d'authentification</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Aucune (authentification locale)</target>
@@ -3599,6 +3595,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3900,8 +3902,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Il semblerait que votre serveur n'utilise par le protocole HTTPS. Vous devez activer TLS sur votre serveur pour pouvoir en suivre d'autres.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Masquer des domaines</target>
@@ -5853,11 +5855,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">CHAÎNES</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
         <target>Ce compte n'a pas de chaîne vidéo.</target>
@@ -5896,6 +5895,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Voulez-vous vraiment supprimer <x id="PH" equiv-text="videoChannel.displayName"/> ? Cela supprimera <x id="PH_1" equiv-text="videoChannel.videosCount"/> les vidéos mises en ligne sur cette chaîne, et vous ne pourrez pas créer une autre chaine avec le même nom (<x id="PH_2" equiv-text="videoChannel.name"/>) !</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6415,13 +6420,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="6979021199788941693">
         <source>Your message has been sent.</source>
         <target>Votre message a été envoyé.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Vous avez déjà rempli ce formulaire récemment</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Vidéos du compte</target>
@@ -6466,13 +6471,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">
           <x id="PH"/> comptes abonnés
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Signaler ce compte</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDÉOS</target>
@@ -6482,31 +6487,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533">
         <source>Username copied</source>
         <target>Nom d'utilisateur·rice copié</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 abonné</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> abonnés</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Les instances que vous suivez</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Les instances qui vous suivent</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Audio seulement</target>
@@ -6556,6 +6551,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target>Auto (avec ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6704,18 +6705,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Un domaine est requis.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Les domaines renseignés sont invalides.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Les domaines renseignés contiennes des doublons.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Illimité</target>
@@ -6883,24 +6900,50 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <x id="PH"/> supprimé des abonné·e·s de votre instance
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> n'est pas valide
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Requête·s envoyée·s !</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Voulez-vous vraiment vous désabonner de <x id="PH"/> ?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Arrêter le suivi</target>
@@ -6909,8 +6952,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>Vous n'êtes plus abonné·e à <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>activé</target>
@@ -7382,9 +7425,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Erreur</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Journaux standards</target>
@@ -7425,16 +7468,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Mettre à jour le mot de passe utilisateur·rice</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Instances suivies</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Instances qui nous suivent</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Utilisateur·rice <x id="PH"/> mis·e à jour.</target>
@@ -7470,16 +7505,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Fédération</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Instances que vous suivez</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Instances qui vous suivent</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Les vidéos seront supprimées, les commentaires seront marqués supprimés.</target>
@@ -7510,6 +7537,24 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Définir l'adresse de courriel comme vérifiée</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
@@ -7814,13 +7859,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1137937154872046253">
         <source>Video channel <x id="PH"/> created.</source>
         <target>Chaîne vidéo <x id="PH"/> créée.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Ce nom existe déjà sur cette instance.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Chaîne vidéo <x id="PH"/> mise à jour.</target>
@@ -7841,11 +7886,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Bannière supprimée.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Merci de saisir le nom de la chaîne vidéo ( <x id="PH"/>) pour confirmer</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Chaîne vidéo <x id="PH"/> supprimée.</target>
@@ -7997,6 +8038,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target>Requête de changement de propriété envoyée.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8095,7 +8142,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>S'abonner à ce compte</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">LISTES DE LECTURE</target>
@@ -8142,34 +8189,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Aller voir mes abonnements</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Aller voir mes vidéos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Aller voir mes imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Aller voir mes chaînes</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Impossible de récupérer les identifiants du Client OAuth : <x id="PH" equiv-text="error.text"/>. Assurez-vous d'avoir correctement configuré PeerTube (dossier config/), en particulier la section "serveur web".</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Vous devez vous reconnecter.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Raccourcis clavier :</target>
@@ -8182,6 +8229,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8210,38 +8263,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>Nom d'utilisateur ou mot de passe incorrects.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Votre compte est bloqué.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">toute langue</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">cacher</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">flouter</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">afficher</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Inconnu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Votre mot de passe a été réinitialisé avec succès !</target>
@@ -9825,18 +9878,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>Trop de tentatives, merci de réessayer dans <x id="PH"/> minutes.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Trop d'essais. Merci de réessayer plus tard.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Le serveur rencontre une erreur. Merci de réessayer plus tard.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Abonné·e à toutes les chaînes actuelles de <x id="PH"/>. Vous serez avertis·es de toutes leurs nouvelles vidéos.</target>
@@ -10419,35 +10472,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275">
         <source>Your video was uploaded to your account and is private.</source>
         <target>Votre vidéo a été téléversée sur votre compte et elle est privée.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Les données associées (étiquettes, description, etc.) seront par contre perdues ; êtes-vous sûr·e de vouloir quitter cette page ?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Votre vidéo n'est pas encore téléversé ; êtes-vous sûr·e de vouloir quitter cette page ?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Mise en ligne</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload <x id="PH"/> </source>
         <target>Téléverser 
           <x id="PH"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Vidéo publiée.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>Vous n'avez pas sauvegardé vos modifications ! Si vous quittez la page, vous les perdrez.</target>
@@ -10494,28 +10547,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Cette vidéo n'est pas disponible sur cette instance ? Voulez-vous être redirigé sur l'instance d'origine : &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a> ?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Redirection</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Cette vidéo contient du contenu sensible. Êtes-vous sûr·e de vouloir la regarder ?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Contenu explicite ou sensible</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Suivant</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Annuler</target>
@@ -10524,63 +10577,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">La lecture automatique est suspendue</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Entrer/sortir du mode plein écran (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Lecture/Pause de la vidéo (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Désactiver/Activer le son de la vidéo (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Sauter à un pourcentage de la vidéo : 0 est 0 % et 9 est 90 % (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Augmenter le volume (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Diminuer le volume (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Avancer la vidéo (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Reculer la vidéo (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Augmenter la vitesse de lecture (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Diminuer la vitesse de lecture (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Naviguer dans la vidéo image par image (nécessite le focus sur le lecteur)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>J’aime cette vidéo</target>
index 2f5aee9c302020b4b4a7acfd229c759bcf2bd813..671c35c0e7a538526c040ba4fb60460841ffaf8b 100644 (file)
       <trans-unit id="187187500641108332" datatype="html">
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Eachdraidh dhe na choimhead mi air</target>
       <trans-unit id="5674286808255988565" datatype="html">
         <source>Create</source>
         <target state="translated">Cruthaich</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">video</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">Tha tòcan prìobhaideach am broinn a’ cheangail a leanas agus cha bu chòir dhut a cho-roinneadh le duine sam bith.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">Chaidh thu thar cuota nam videothan agad leis a’ video seo (meud a’ video: <x id="PH" equiv-text="videoSizeBytes"/>, ’ga chleachdadh: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, cuota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">Bheir a’ video seo thar cuota làitheil nam videothan agad thu (meud a’ video: <x id="PH" equiv-text="videoSizeBytes"/>, ’ga chleachdadh: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, cuota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">fo-thiotalan</target>
       <trans-unit id="2602586221576511475" datatype="html">
         <source>Video quota</source>
         <target state="translated">Cuota de videothan</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="translated">Gun chuingeachadh <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> gach latha)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target state="translated">Co-nasgadh</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Sguir dheth</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="translated">Toirmisg an cleachdaiche seo</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Faodaidh tu clàradh air an ionstans seo.Gidheadh, dèan cinnteach gun leugh thu na <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Teirmichean<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Teirmichean<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> mus cruthaich thu cunntas. ’S urrainn dhut cuideachd ionstans eile a lorg a fhreagras ris na feumalachdan agad-sa: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Cha cheadaich an t-ionstans seo gun clàraich cleachdaichean ùra aig an àm seo, ’s urrainn dhut sùil a thoirt air na <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Teirmichean<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> airson barrachd fiosrachaidh no ionstans a lorg a bheir comas clàraidh cunntais dhut agus na videothan agad a luchdadh suas an-siud. Lorg an t-ionstans agad fhèin am measg càich: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729" datatype="html">
         <source>User</source>
         <target state="translated">Cleachdaiche</target>
         <source>Username or email address</source>
         <target state="translated">Ainm-cleachdaiche no seòladh puist-d</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429" datatype="html">
         <source>Password</source>
         <target state="translated">Facal-faire</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Briog an-seo airson am facal-faire agad ath-shuidheachadh</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Dìochuimhnich mi am facal-faire agam</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Ma nì thu clàradh a-steach air cunntas, ’s urrainn dhut susbaint fhoillseachadh</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966" datatype="html">
         <source>Login</source>
         <target state="translated">Clàraich a-steach</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">No clàraich a-steach le</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367" datatype="html">
         <source>Forgot your password</source>
         <target state="translated">Na dhìochuimhnich thu am facal-faire agad?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Tha sinn duilich ach chan urrainn dhut am facal-faire agad aiseag air sgàth ’s nach do rèitich rianaire an ionstans seo siostam puist-d PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Cuir a-steach an seòladh puist-d agad agus cuiridh sinn ceangal thugad gus am facal-faire agad ath-shuidheachadh.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1084,26 +1102,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664" datatype="html">
         <source>Email</source>
         <target state="translated">Post-d</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610" datatype="html">
         <source>Email address</source>
         <target state="translated">Seòladh puist-d</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Ath-shuidhich</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">air an ionstans seo</target>
@@ -1427,9 +1445,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902" datatype="html">
         <source>Create an account</source>
         <target state="translated">Cruthaich cunntas</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">Na videothan agam</target>
@@ -1496,10 +1514,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDEOTHAN</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">Co-ruith nan obraichean ion-phortaidh</target>
@@ -1578,8 +1596,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">’S e poit-tì a th’ annam</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Seo mearachd.</target>
@@ -1672,8 +1690,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">Tha am meadhan ro mhòr airson an fhrithealaiche. Cuir fios gun rianaire agad ma tha thu airson crìoch a’ mheud a mheudachadh.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">LORG UILE-CHOITCHEANN</target>
@@ -2409,8 +2427,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="translated">Tha an luchdadh suas ’ga cumail air ais</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Tha sinn duilich ach chaidh gleus an luchdaidh suas a chur à comas dhan chunntas agad. Ma tha thu airson videothan a chur ris, feumaidh rianaire an glas a thoirt far a’ chuota agad.</target>
@@ -3078,11 +3096,7 @@ The link will expire within 1 hour.</source>
         <target state="translated">ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="translated">Làimhsichear neach-leantainn</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="translated">Staid</target>
@@ -3148,11 +3162,7 @@ The link will expire within 1 hour.</source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action }}"/> </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="translated">Òstair</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Tha anabarrachd ceadaichte <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3161,9 +3171,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Na lean tuilleadh</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Fosgail an t-ionstans ann an taba ùr</target>
@@ -3174,28 +3184,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Cha deach òstair a lorg a fhreagras ris na criathragan làithreach.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Chan eil an t-ionstans agad a’ leantainn air dad sam bith.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">A’ sealltainn <x id="INTERPOLATION"/> gu <x id="INTERPOLATION_1"/> à <x id="INTERPOLATION_2"/> òstair(ean)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Lean air àrainnean</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Lean air ionstansan</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">Gnìomh</target>
@@ -3245,11 +3247,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023" datatype="html">
         <source>Username</source>
         <target state="translated">Ainm-cleachdaiche</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">m.e.: mairi_mhor</target>
@@ -3277,9 +3279,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target state="translated">Dreuchd</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Tha an tar-chòdachadh an comas. Cha dèid ach meud <x id="START_TAG_STRONG"/>tùsail<x id="CLOSE_TAG_STRONG"/> nam videothan a chunntadh mu choinneamh a’ chuota. <x id="LINE_BREAK"/> B’ urrainn dhan chleachdaiche seo mu <x id="INTERPOLATION"/> a luchdadh suas air a char as motha. </target>
@@ -3296,15 +3298,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Plugan dearbh-aithneachaidh</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Chan eil gin (dearbh-aithneachadh ionadail)</target>
@@ -3555,6 +3551,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3854,8 +3856,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Tha coltas nach eil thu air frithealaiche HTTPS. Feumaidh am frithealaiche agad TLS a chur an comas mus lean e air frithealaichean eile.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Mùch àrainnean</target>
@@ -5794,11 +5796,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">SEANAILEAN</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">Chan eil seanail aig a’ chunntas seo.</target>
@@ -5837,6 +5836,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">A bheil thu cinnteach gu bheil thu airson <x id="PH" equiv-text="videoChannel.displayName"/> a sguabadh às?Sguabaidh seo às <x id="PH_1" equiv-text="videoChannel.videosCount"/> video(than) a chaidh a luchdadh suas dhan t-seanail seo ’s chan urrainn dhut seanail eile air a bheil an t-aon ainm (<x id="PH_2" equiv-text="videoChannel.name"/>) a chruthachadh!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6350,13 +6355,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="6979021199788941693" datatype="html">
         <source>Your message has been sent.</source>
         <target state="translated">Chaidh an teachdaireachd agad a chur.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="translated">Chuir thu am foirm seo a-null o chionn greis mu thràth</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Videothan a’ chunntais</target>
@@ -6399,13 +6404,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="4856575356061361269" datatype="html">
         <source><x id="PH"/> direct account followers </source>
         <target state="translated"><x id="PH"/> luchd-leantainn dìreach a’ chunntais </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Dèan gearan mun chunntas seo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDEOTHAN</target>
@@ -6415,31 +6420,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">Chaidh lethbhreac a dhèanamh dhen ainm-chleachdaiche</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">Rinn 1 fo-sgrìobhadh air</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated">Rinn <x id="PH"/> fo-sgrìobhadh air</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Na h-ionstansan air a leanas tu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Na h-ionstansan a tha a’ leantainn ort</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Fuaim a-mhàin</target>
@@ -6489,6 +6484,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target state="translated">Fèin-obrachail (slighe ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6637,18 +6638,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Tha àrainn-lìn riatanach.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Chaidh àrainn mhì-dhligheach a chur a-steach.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Tha dùblachadh am measg nan àrainnean a chaidh a chur a-steach.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="translated">Gun chrìoch</target>
@@ -6802,22 +6819,48 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source><x id="PH"/> removed from instance followers </source>
         <target state="translated">Chaidh <x id="PH"/> a thoirt air falbh ach nach lean e air an ionstans tuilleadh </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="translated">Chan eil <x id="PH"/> dligheach </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="translated">Chaidh iarrtas(an) leantainn a chur!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="translated">A bheil thu cinnteach gu bheil thu airson sgur de leantainn air <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Na lean tuilleadh</target>
@@ -6826,8 +6869,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926" datatype="html">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target state="translated">Chan eil thu a’ leantainn air <x id="PH"/> tuilleadh.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="translated">an comas</target>
@@ -7291,9 +7334,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="translated">Mearachd</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Logaichean àbhaisteach</target>
@@ -7334,16 +7377,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Ùraich facal-faire a’ chleachdaiche</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Liosta dhen fheadhainn air a leanas tu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Liosta dhen fheadhainn a leanas ort</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Chaidh an cleachdaiche <x id="PH"/> ùrachadh.</target>
@@ -7379,16 +7414,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Co-nasgadh</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Na h-ionstansan air a leanas tu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Na h-ionstansan a tha a’ leantainn ort</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Thèid na videothan a sguabadh às agus comharra sguabaidh às a chur ris na beachdan.</target>
@@ -7419,6 +7446,24 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Suidhich gun deach am post-d a dhearbhadh</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
@@ -7717,13 +7762,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1137937154872046253" datatype="html">
         <source>Video channel <x id="PH"/> created.</source>
         <target state="translated">Chaidh seanail video <x id="PH"/> a chruthachadh.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="translated">Tha an t-ainm seo ann air an ionstans seo mu thràth.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="translated">Chaidh seanail video <x id="PH"/> ùrachadh.</target>
@@ -7744,11 +7789,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Chaidh a’ bhratach a sguabadh às.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="translated">Sgrìobh ainm-taisbeanaidh na seanail video (<x id="PH"/>) gus a dhearbhadh</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="translated">Chaidh seanail video <x id="PH"/> a sguabadh às.</target>
@@ -7896,6 +7937,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target state="translated">Chaidh iarrtas a chur air atharrachadh an t-seilbh.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -7994,7 +8041,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Fo-sgrìobh air a’ chunntas</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">LIOSTAICHEAN-CLUICH</target>
@@ -8041,34 +8088,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="translated">Tadhail air na fo-sgrìobhaidhean agam</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="translated">Tadhail air na videothan agam</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="translated">Tadhail air na h-ion-phortaidhean agam</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="translated">Tadhail air na seanailean agam</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Cha b’ urrainn dhuinn an teisteas cliant OAuth fhaighinn: <x id="PH" equiv-text="error.text"/>. Dèan cinnteach gun do rèitich thu PeerTube mar bu chòir (sa phasgan config/) ’s gu sònraichte an earrann "webserver".</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="translated">Feumaidh tu ceangal ris a-rithist.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="translated">Ath-ghoiridean a’ mheur-chlàir:</target>
@@ -8081,6 +8128,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8109,38 +8162,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target state="translated">Chan eil an t-ainm-cleachdaiche no chan eil am facal-faire mar bu chòir.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Chaidh an cunntas agad a bhacadh.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">cànan sam bith</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">falaich</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">sgleò</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">taisbean</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Chan eil fhios</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="translated">Chaidh am facal-faire agad ath-shuidheachadh!</target>
@@ -9698,18 +9751,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070" datatype="html">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target state="translated">Cus oidhirpean, feuch ris a-rithist an ceann <x id="PH"/> mionaid(ean).</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="translated">Cus oidhirpean, feuch ris a-rithist an ceann greis.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="translated">Mearachd an fhrithealaiche. Feuch ris a-rithist an ceann greis.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Fhuair thu fo-sgrìobhadh air na seanailean làithreach uile aig <x id="PH"/>. Gheibh thu brathan-naidheachd mu na videothan ùra aca uile.</target>
@@ -10294,33 +10347,33 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275" datatype="html">
         <source>Your video was uploaded to your account and is private.</source>
         <target state="translated">Chaidh a’ video agad a luchdadh suas dhan chunntas agad ’s tha e prìobhaideach.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="translated">Ach thèid dàta sam bith nach deach a shàbhaladh (tagaichean, tuairisgeulan…) air chall, a bheil thu cinnteach gu bheil thu airson an duilleag seo fhàgail?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="translated">Cha deach a’ video agad a luchdadh suas fhathast, a bheil thu cinnteach gu bheil thu airson an duilleag seo fhàgail?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Luchdaich suas</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">Luchdaich suas <x id="PH"/> </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="translated">Chaidh a’ video fhoillseachadh.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119" datatype="html">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target state="translated">Tha atharraichean gun sàbhaladh agad! Ma dh’fhalbhas tu, thèid na h-atharraichean agad air chall.</target>
@@ -10387,28 +10440,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Chan eil a’ video seo ri fhaighinn air an ionstans seo. A bheil thu airson ’s gun dèid d’ ath-stiùireadh dhan ionstans thùsail: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Ath-stiùireadh</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="translated">Tha susbaint sa video seo a tha iomchaidh do dh’inbhich a-mhàin. A bheil thu cinnteach gu bheil thu airson coimhead air?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="translated">Susbaint do dh’inbhich</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Ri thighinn</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Sguir dheth</target>
@@ -10417,63 +10470,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">Chaidh a’ chluiche fhèin-obrachail a chur à rèim</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">A-steach/a-mach às a’ mhodh làn-sgrìn (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Cluich a’ video/Cuir ’na stad e (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Mùch/Dì-mhùch a’ video (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Thoir leum gu ceudad dhen video: Bheir 0 gu 0% thu agus 9 gu 90% (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Meudaich àirde na fuaime (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Lùghdaich àirde na fuaime (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Sir air adhart sa video (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Sir air ais sa video (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Dèan a’ chluich nas luaithe (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Dèan a’ chluich nas maille (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Seòl tron video frèam air fhrèam (bidh an cluicheadair feumach air an fhòcas)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="translated">Comharraich gur toigh leat a’ video</target>
index 3a4a121be3aeedfdde08aa0af98585172e73d845..751265c370d2104c39e93218f39f8a2216aa5af3 100644 (file)
         <target state="translated">
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Historial de visualizacións</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Crear</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">vídeo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">A seguinte ligazón contén un token privado e non deberías compartilo con ninguén.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">Este vídeo fai que superes a túa cota de vídeo (tamaño do vídeo: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, cota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">Con este vídeo superas a túa cota diaria de vídeo (tamaño do vídeo: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, cota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">subtítulos</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Cota de vídeo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Sen límite <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> diario)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target state="translated">Federación</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
       <trans-unit id="2159130950882492111">
         <source>Cancel</source>
         <target>Cancelar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Vetar esta usuaria</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Esta instancia ten o rexistro aberto. Non obstante, pon tino en comprobar <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Termos<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Os Termos<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> antes de crear unha conta. Podes atopar outra instancia máis acorde ás túas necesidades en: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Actualmente esta instancia non permite abrir unha conta, podes ler os <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Termos<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> para saber máis ou atopar outra instancia co rexistro aberto e poder subir alí os teus vídeos. Atopa a túa entre varias opcións en: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Usuaria</target>
         <source>Username or email address</source>
         <target>Nome de usuaria ou enderezo de correo</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Contrasinal</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Preme aquí e restablece o contrasinal</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Esquecín o contrasinal</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Se estás conectada poderás publicar o teu contido</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Conectar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Ou conéctate con</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Esqueceu o contrasinal</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Lamentámolo, non podes recuperar o contrasinal porque a administración da instancia non configurou o sistema de email de PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Escribe o teu email e enviarémosche unha ligazón para restablecer o contrasinal.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1099,26 +1117,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Correo electrónico</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Enderezo de correo electrónico</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Restablecer</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">nesta instancia</target>
@@ -1448,9 +1466,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>Crear unha conta</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">Vídeos</target>
@@ -1517,10 +1535,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VÍDEOS</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">Concurrencia de tarefas de importación</target>
@@ -1599,8 +1617,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">Son unha teteira</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">É un erro.</target>
@@ -1693,8 +1711,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">O multimedia é demasiado grande para o servidor. Contacta coa administración se desexas que aumenten o límite.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">BUSCA GLOBAL</target>
@@ -2434,8 +2452,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="translated">Subida agardando</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Lamentámolo, a túa conta non permite subir contidos. Se queres engadir vídeos, unha administradora debe aumentar a túa cota.</target>
@@ -3123,11 +3141,7 @@ The link will expire within 1 hour.</source>
         <target state="translated">ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="translated">Xestión da seguidora</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="translated">Estado</target>
@@ -3193,11 +3207,7 @@ The link will expire within 1 hour.</source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action }}"/> </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="translated">Servidor</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Redundancia permitida <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3206,9 +3216,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Deixar de seguir</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Abrir instancia en nova lapela</target>
@@ -3219,28 +3229,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Non se atoparon servidores co criterio do filtro.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">A túa instancia non segue a ninguén.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Mostrando <x id="INTERPOLATION"/> a <x id="INTERPOLATION_1"/> de <x id="INTERPOLATION_2"/> servidores</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Seguir dominios</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Seguir instancias</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">Acción</target>
@@ -3290,11 +3292,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Nome de usuaria</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">ex. ugio_ben</target>
@@ -3322,9 +3324,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target state="translated">Rol</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Recodificación activada. A cota de vídeo só ten en conta o tamaño <x id="START_TAG_STRONG"/>orixinal<x id="CLOSE_TAG_STRONG"/> do vídeo. <x id="LINE_BREAK"/> Como moito, esta usuaria podería subir ~ <x id="INTERPOLATION"/>. </target>
@@ -3341,15 +3343,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Complemento Auth</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Ningún (autenticación local)</target>
@@ -3600,6 +3596,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3899,8 +3901,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Semella que non estás nun servidor HTTPS. O teu servidor web precisa ter TLS activado para poder seguir servidores.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Acalar dominios</target>
@@ -5837,11 +5839,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">CANLES</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">Esta conta non ten canles.</target>
@@ -5880,6 +5879,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Desexas eliminar <x id="PH" equiv-text="videoChannel.displayName"/>? Así eliminarás <x id="PH_1" equiv-text="videoChannel.videosCount"/> vídeos subidos a esta canle, e non poderás volver a crear outra canle co mesmo nome (<x id="PH_2" equiv-text="videoChannel.name"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6395,13 +6400,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="6979021199788941693" datatype="html">
         <source>Your message has been sent.</source>
         <target state="translated">A túa mensaxe foi enviada.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="translated">Xa enviaras este formulario recentemente</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Vídeos da conta</target>
@@ -6444,13 +6449,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="4856575356061361269" datatype="html">
         <source><x id="PH"/> direct account followers </source>
         <target state="translated"><x id="PH"/> seguidoras directas da conta </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Denunciar esta conta</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VÍDEOS</target>
@@ -6460,31 +6465,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">Nome de usuaria copiado</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 subscritora</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> subscritoras</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Instancias que segues</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Instancias que te seguen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Só audio</target>
@@ -6534,6 +6529,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target state="translated">Auto (vía ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6682,18 +6683,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Requírese un dominio.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">O dominio escrito non é válido.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Hai duplicados nos dominios escritos.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="translated">Sen límite</target>
@@ -6857,22 +6874,48 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source><x id="PH"/> removed from instance followers </source>
         <target state="translated"><x id="PH"/> eliminada das seguidoras da instancia </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="translated"><x id="PH"/> non é válido </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="translated">Solicitude(s) de seguimento enviada(s)!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="translated">Desexas deixar de seguir a <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Deixar de seguir</target>
@@ -6881,8 +6924,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926" datatype="html">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target state="translated">Xa non segues <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="translated">activado</target>
@@ -7346,9 +7389,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="translated">Erro</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Rexistros estándar</target>
@@ -7389,16 +7432,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Actualizar contrasinal da usuaria</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Lista de seguimentos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Lista de seguidoras</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Usuaria <x id="PH"/> actualizada.</target>
@@ -7434,16 +7469,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Federación</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Instancias que segues</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Instancias que te seguen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Os vídeos serán eliminados, os comentarios serán soterrados.</target>
@@ -7474,6 +7501,24 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Establecer email como Verificado</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
@@ -7772,13 +7817,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1137937154872046253" datatype="html">
         <source>Video channel <x id="PH"/> created.</source>
         <target state="translated">Creada a canle de vídeo <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="translated">Este nome xa existe nesta instancia.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="translated">Actualizada a canle de vídeo <x id="PH"/>.</target>
@@ -7799,11 +7844,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Cabeceira eliminada.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="translated">Escribe o nome a mostrar para a canle de vídeo (<x id="PH"/>) para confirmar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="translated">Eliminada a canle de vídeo <x id="PH"/>.</target>
@@ -7951,6 +7992,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target state="translated">Enviouse a solicitude de cambio de propiedade.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8049,7 +8096,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Subscribirse á conta</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">LISTAXES</target>
@@ -8096,34 +8143,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="translated">Ir ás miñas subscricións</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="translated">Ir ós meus vídeos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="translated">Ir ás importacións</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="translated">Ir ás miñas canles</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Non se poden obter as credenciais OAuth Client: <x id="PH" equiv-text="error.text"/>. Asegúrate de ter configurado correctamente PeerTube (config/ directory), en particular a sección "webserver".</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="translated">Tes que reconectar.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="translated">Atallos de teclado:</target>
@@ -8136,6 +8183,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8164,38 +8217,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target state="translated">Usuaria ou contrasinal incorrectos.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">A túa conta está bloqueada.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">tódolos idiomas</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">agochar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">esborranchar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">mostrar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Descoñecido</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="translated">Restableceuse correctamente o contrasinal!</target>
@@ -9753,18 +9806,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070" datatype="html">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target state="translated">Demasiados intentos, inténtao outra vez tras <x id="PH"/> minutos.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="translated">Demasiados intentos, inténtao máis tarde.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="translated">Erro do servidor. Inténtao máis tarde.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Subscrita a tódalas canles de <x id="PH"/>. Recibirás notificación de tódolos seus vídeos.</target>
@@ -10339,33 +10392,33 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275" datatype="html">
         <source>Your video was uploaded to your account and is private.</source>
         <target state="translated">O vídeo subeuse á túa conta e é privado.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="translated">Pero os datos asociados (etiquetas, descrición...) perderanse, queres saír igualmente desta páxina?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="translated">O vídeo aínda non se subiu, desexas realmente saír desta páxina?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Subir</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">Subir <x id="PH"/> </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="translated">Vídeo publicado.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119" datatype="html">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target state="translated">Tes cambios sen gardar! Se saes perderás os cambios.</target>
@@ -10412,28 +10465,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Este vídeo non está dispoñible na túa instancia. Queres ser redirixida á instancia orixinal: &lt;a href="<x id="PH"/>"><x id="PH_1"/>/a>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Redirección</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="translated">Este vídeo contén contido explicito ou adulto. Tes certeza de querer velo?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="translated">Contido explícito ou adulto</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">A seguir</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Cancelar</target>
@@ -10442,63 +10495,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">Reprodución automática suspendida</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Entrar/Saír da pantalla completa (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Reproducir/Pausar o vídeo (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Acalar/Restablecer o vídeo (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Saltar a unha porcentaxe do vídeo: 0 é 0% e 9 é 90% (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Aumentar volume (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Diminuír volume (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Saltar adiante no vídeo (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Saltar atrás no vídeo (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Aumentar taxa de reprodución (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Diminuir taxa de reprodución (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Navegar fotograma a fotograma (require foco no reprodutor)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="translated">Gústame o vídeo</target>
index 1a7e3e3e9a2103b269ffb3c701f66483987ba108..884240b287a7e6da34d8a83776a80a1c78471622 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Saját megtekintési előzmények</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">videó</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="new"> The following link contains a private token and should not be shared with anyone. </target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">feliratok</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Videokvóta</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Korlátlan <x id="START_TAG_NG_CONTAINER"/>(napi <x id="INTERPOLATION"/>)<x id="CLOSE_TAG_NG-CONTAINER"/></target>
         <source>Federation</source>
         <target state="translated">Föderáció</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="translated">követő</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Felhasználó kitiltása</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Felhasználó</target>
         <source>Username or email address</source>
         <target>Felhasználónév vagy e-mail-cím</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Jelszó</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Kattintson ide a jelszava visszaállításához</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Bejelentkezés</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Vagy jelentkezzen be ezzel:</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Elfelejtett jelszó</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Sajnáljuk, de nem tudja helyreállítani a jelszavát, mert a példány rendszergazdája nem állította be a PeerTube levelezőrendszerét.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1019,26 +1037,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>E-mail</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>E-mail-cím</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">ezen a példányon</target>
@@ -1364,7 +1382,7 @@ The link will expire within 1 hour.</source>
         <target>Fiók létrehozása</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1421,7 +1439,7 @@ The link will expire within 1 hour.</source>
         <source>VIDEOS</source>
         <target state="translated">VIDEÓK</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1500,7 +1518,7 @@ The link will expire within 1 hour.</source>
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1593,8 +1611,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">A média túl nagy ehhez a kiszolgálóhoz. Lépjen kapcsolatba a rendszergazdával, ha növelni szeretné a méretkorlátot.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2299,7 +2317,7 @@ The link will expire within 1 hour.</source>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Sajnáljuk, a feltöltési funkció tiltott a fiókjában. Ha videókat akar hozzáadni, akkor egy rendszergazdának fel kell oldania a kvótája zárolását.</target>
@@ -2979,11 +2997,7 @@ The link will expire within 1 hour.</source>
         <target>Azonosító</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="translated">Követőkezelő</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Állapot</target>
@@ -3051,11 +3065,7 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="translated">Gép</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Redundancia megengedett <x id="START_TAG_P-SORTICON"/><x id="CLOSE_TAG_P-SORTICON"/></target>
@@ -3065,7 +3075,7 @@ The link will expire within 1 hour.</source>
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Példány megnyitása új lapon</target>
@@ -3076,28 +3086,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Nem található a jelenlegi szűrőkre illeszkedő gép.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Az Ön példánya nem követ senkit sem.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated"><x id="INTERPOLATION"/> – <x id="INTERPOLATION_1"/> / <x id="INTERPOLATION_2"/> gép megjelenítése</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Tartományok követése</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3146,11 +3148,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Felhasználónév</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3178,9 +3180,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Szerep</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Az átkódolás engedélyezve van. A videokvóta csak az <x id="START_TAG_STRONG"/>eredeti <x id="CLOSE_TAG_STRONG"/> videó méretét veszi figyelembe. <x id="LINE_BREAK"/> Ez a felhasználó legfeljebb ~ <x id="INTERPOLATION"/>-ot tölthet fel. </target>
@@ -3197,15 +3199,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3445,6 +3441,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3741,8 +3743,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Úgy tűnik, hogy nem HTTPS kiszolgálón van. A TLS-nek bekapcsolva kell lennie a webkiszolgálóján, hogy kiszolgálókat követhessen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Tartományok némítása</target>
@@ -5680,11 +5682,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">Ennek a fióknak nincsenek csatornái.</target>
@@ -5723,6 +5722,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Biztos, hogy törli a(z) <x id="PH" equiv-text="videoChannel.displayName"/> csatornát? Ez törli a csatornára feltöltött <x id="PH_1" equiv-text="videoChannel.videosCount"/> videót, és nem hozhat létre még egy csatornát ugyanezzel a névvel (<x id="PH_2" equiv-text="videoChannel.name"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6236,12 +6241,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Your message has been sent.</source>
         <target>Az üzenete el lett küldve.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Már nemrég elküldte ezt az űrlapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Fiók videói</target>
@@ -6285,13 +6290,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">
           <x id="PH"/> közvetlen fiókkövető
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Fiók jelentése</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDEÓK</target>
@@ -6301,29 +6306,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">Felhasználónév lemásolva</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 feliratkozó</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> feliratkozó</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Példányok, amiket követ</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Példányok, amik követik</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Csak hang</target>
@@ -6373,6 +6370,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target>Automatikus (ffmpeg által)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6513,18 +6516,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Tartomány szükséges.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">A megadott tartományok érvénytelenek.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">A megadott tartományok kettőzéseket tartalmaznak.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="translated">Korlátlan</target>
@@ -6684,24 +6703,50 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <x id="PH"/> eltávolítva a példány követőiből
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="translated">
           <x id="PH"/> nem érvényes
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="translated">Követési kérések elküldve!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="translated">Biztos, hogy megszünteti a(z) <x id="PH"/> követését?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Követés megszüntetése</target>
@@ -6710,8 +6755,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926" datatype="html">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target state="translated">Többé már nem követi a(z) <x id="PH"/> gépet.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="translated">engedélyezve</target>
@@ -7183,9 +7228,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="translated">Hiba</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Szabványos naplók</target>
@@ -7226,16 +7271,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Felhasználó jelszavának frissítése</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Követési lista</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Követők listája</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated"><x id="PH"/> felhasználó frissítve.</target>
@@ -7271,16 +7308,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Föderáció</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Követett példányok</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Követő példányok</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">A videók törölve lesznek, a hozzászólások el lesznek temetve.</target>
@@ -7311,7 +7340,25 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">E-mail beállítása ellenőrzöttre</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="translated">Nem tilthatja ki a rendszergazdát.</target>
@@ -7616,13 +7663,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1137937154872046253" datatype="html">
         <source>Video channel <x id="PH"/> created.</source>
         <target state="translated">A(z) <x id="PH"/> videócsatorna létrehozva.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="translated">Ez a név már létezik ebben a példányban.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="translated">A(z) <x id="PH"/> videocsatorna frissítve.</target>
@@ -7643,11 +7690,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="translated">Írja be a videócsatorna megjelenített nevét ( <x id="PH"/>) a megerősítéshez</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="translated">A(z) <x id="PH"/> videócsatorna törölve.</target>
@@ -7796,6 +7839,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target state="translated">Tulajdonjog-változtatási kérelem elküldve.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -7894,7 +7943,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Feliratkozás a fiókra</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="new">PLAYLISTS</target>
@@ -7941,34 +7990,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="translated">Ugrás a feliratkozásaimhoz</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="translated">Ugrás a videóimhoz</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="translated">Ugrás az importálásaimhoz</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="translated">Ugrás a csatornáimhoz</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Az OAuth kliens hitelesítő adatai nem kérhetők le: <x id="PH" equiv-text="error.text"/>. Győződjön meg róla, hogy helyesen állította be a PeerTube-ot (konfiguráció / könyvtár), különösképpen a „webserver” szakaszban.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="translated">Újra kell csatlakoznia.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="translated">Gyorsbillentyűk:</target>
@@ -7979,6 +8028,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8001,39 +8056,39 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target state="translated">Helytelen felhasználónév vagy jelszó.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">A fiókja le van tiltva.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">bármely nyelv</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">elrejtés</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">homályosítás</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">megjelenítés</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Ismeretlen</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="translated">A jelszava sikeresen vissza lett állítva!</target>
@@ -9606,18 +9661,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070" datatype="html">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target state="translated">Túl sok próbálkozás. Próbálja meg újra <x id="PH"/> perc múlva.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="translated">Túl sok próbálkozás. Próbálja meg újra később.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="translated">Kiszolgálóhiba. Próbálja újra később.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Feliratkozva <x id="PH"/> összes jelenlegi csatornájára. Értesítést fog kapni az összes új videójukról.</target>
@@ -10206,35 +10261,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="translated">A videó fel lett töltve a fiókjába, és személyes.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="translated">De a kapcsolódó adatok (címkék, leírás, …) el fognak veszni. Biztosan el szeretné hagyni ezt az oldalt?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="translated">A videó még nincs feltöltve. Biztosan el szeretné hagyni ezt az oldalt?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Feltöltés</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">
           <x id="PH"/> feltöltése
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="translated">Videó közzétéve.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10284,27 +10339,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Ez a videó nem érhető el ezen a példányon. Szeretné, hogy átirányítsuk a forráspéldányhoz: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Átirányítás</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="translated">Ez a videó felnőtt vagy korhatáros tartalmat tartalmaz. Biztosan meg szeretné nézni?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="translated">Felnőtt vagy korhatáros tartalom</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Legközelebb</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Mégse</target>
@@ -10314,62 +10369,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="translated">Az automatikus lejátszás fel van függesztve</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Teljes képernyőre váltás vagy normál méret (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">A videó lejátszása vagy szüneteltetése (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">A videó némítása vagy a némítás visszavonása (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Kihagyás a videó százalékáig: a 0 jelentése 0%, a 9 jelentése 90% (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">A hangerő növelése (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">A hangerő csökkentése (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">A videó előretekerése (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">A videó visszatekerése (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Lejátszási sebesség növelése (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Lejátszási sebesség csökkentése (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Navigálás a videóban képkockánként (lejátszófókuszt igényel)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="translated">A videó kedvelése</target>
index a66051666a2cd06703be9542d0b60a12927b7c8d..0f196bfbf1a4e8fae2705013027236f1be896d51 100644 (file)
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">La mia cronologia di visualizzazione</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Quota video</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Illimitato <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> al giorno)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <source>Federation</source>
         <target state="translated">Federazione</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="translated">follower</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Espelli questo utente</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Questa istanza consente la registrazione. Tuttavia, fai attenzione a controllare i <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Termini<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Termini<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>prima di creare un account. Puoi anche cercare un'altra istanza per soddisfare le tue esigenze esatte su: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/it/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Attualmente questa istanza non consente la registrazione dell'utente, puoi controllare i <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Termini<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> per maggiori dettagli o per trovare un'istanza che ti dà la possibilità di registrare un'account e caricare i tuoi video lì. Trova il tuo tra più istanze su:<x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/it/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Utente</target>
         <source>Username or email address</source>
         <target>Nome utente o indirizzo email</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Clicca qui per resettare la tua password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Ho dimenticato la mia password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">L'accesso a un account ti consente di pubblicare contenuti</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Accedi</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">O accedi con</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Password dimenticata</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Ci scusiamo, non c'è modo di recuperare la tua password perchè l'amministratore dell'istanza non ha configurato il sistema di email di PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Inserisci il tuo indirizzo email e ti invieremo un link per reimpostare la tua password.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1011,26 +1029,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Email</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Indirizzo email</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Reimposta</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
@@ -1359,7 +1377,7 @@ The link will expire within 1 hour.</source>
         <target>Crea un account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1416,7 +1434,7 @@ The link will expire within 1 hour.</source>
         <source>VIDEOS</source>
         <target state="translated">VIDEO</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1496,7 +1514,7 @@ The link will expire within 1 hour.</source>
         <source>I'm a teapot</source>
         <target state="translated">Sono una teiera</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Questo è un errore.</target>
@@ -1580,8 +1598,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">Il video è troppo grande per il server. Contatta il tuo amministratore se desideri aumentare la dimensione del limite.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2261,7 +2279,7 @@ The link will expire within 1 hour.</source>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Spiacente, la funzionalità di upload è disabilitata per il tuo account. Se vuoi aggiungere video, un amministratore deve sbloccare la tua quota.</target>
@@ -2970,11 +2988,7 @@ The link will expire within 1 hour.</source>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="translated">Gestione seguaci</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Stato</target>
@@ -3040,11 +3054,7 @@ The link will expire within 1 hour.</source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action }}"/> </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Ridondanza consentita <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3054,7 +3064,7 @@ The link will expire within 1 hour.</source>
         <source>Unfollow</source>
         <target state="translated">Smetti di seguire</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Apri l'istanza in una nuova scheda</target>
@@ -3066,27 +3076,19 @@ The link will expire within 1 hour.</source>
         <source>No host found matching current filters.</source>
         <target state="translated">Nessun host trovato corrispondente ai filtri correnti.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">La tua istanza non sta seguendo nessuno.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Mostra <x id="INTERPOLATION"/> a <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Segui domini</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Segui istanze</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3133,11 +3135,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Nome utente</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">esempio: maria_rossi</target>
@@ -3165,9 +3167,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Ruolo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">La transcodifica è abilitata. La quota video prende in considerazione solo <x id="START_TAG_STRONG"/>dimensione<x id="CLOSE_TAG_STRONG"/> originale del video. <x id="LINE_BREAK"/> L'utente dovrebbe poter caricare ~ <x id="INTERPOLATION"/>. </target>
@@ -3184,15 +3186,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3437,7 +3433,13 @@ The link will expire within 1 hour.</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target state="translated">Video commentati</target>
@@ -3726,7 +3728,7 @@ The link will expire within 1 hour.</source>
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Sembra che tu non sia su un server HTTPS. Il tuo server web deve avere TLS attivato per poter seguire i server.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Silenzia domini</target>
@@ -5630,11 +5632,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5668,7 +5667,13 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Vuoi davvero eliminare <x id="PH" equiv-text="videoChannel.displayName"/>? Verranno eliminati <x id="PH_1" equiv-text="videoChannel.videosCount"/> i video caricati in questo canale e non potrai creare un altro canale con lo stesso nome <x id="PH_2" equiv-text="videoChannel.name"/></target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="translated">I miei Canali</target>
@@ -6185,12 +6190,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Your message has been sent.</source>
         <target>Messaggio inviato.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Questo modulo è stato usato di recente</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
@@ -6236,12 +6241,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source><x id="PH"/> direct account followers </source>
         <target state="translated"><x id="PH"/> follower diretti dell'account </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Segnala questo account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       
       <trans-unit id="1504521795586863905" datatype="html">
@@ -6256,27 +6261,19 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Nome utente copiato</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 sottoscrittore</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> sottoscrittori</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Istanze che segui</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Istanze che ti seguono</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Solo audio</target>
@@ -6326,6 +6323,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target>Auto (tramite ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6466,18 +6469,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Il Dominio è richiesto.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">I domini inseriti non sono validi.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">I domini inseriti contengono duplicati.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Illimitato/ti</target>
@@ -6631,24 +6650,50 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source><x id="PH"/> removed from instance followers </source>
         <target state="translated"><x id="PH"/> rimosso dai follower dell'istanza </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> non è valida
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Richiesta/e per seguire (follow) spedita/e!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Vuoi veramente smettere di seguire <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Smetti di seguire (unfollow)</target>
@@ -6657,8 +6702,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>Non stai seguendo piú <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>attivato</target>
@@ -7098,9 +7143,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Errore</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Logs standard</target>
@@ -7141,16 +7186,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Aggiorna password dell' utente</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Elenco seguente</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Elenco followers</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Utente <x id="PH"/> aggiornato.</target>
@@ -7186,16 +7223,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Federazione</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Istanze che segui</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Istanze che ti seguono</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">I video verranno eliminati, i commenti verranno rimossi definitivamente.</target>
@@ -7226,7 +7255,25 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Imposta email come verificata</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
         <target>Non puoi espellere root.</target>
@@ -7530,12 +7577,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Video channel <x id="PH"/> created.</source>
         <target>Il canale video <x id="PH"/> è stato creato.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Questo nome esiste già nell'istanza.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Il canale video <x id="PH"/> è stato aggiornato.</target>
@@ -7550,11 +7597,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Digita il nome visualizzato del canale video ( <x id="PH"/>) per confermare</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Il canale video <x id="PH"/> è stato cancellato.</target>
@@ -7703,6 +7746,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target>Richiesta di cambio proprietario spedita.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -7804,7 +7853,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Iscriversi all'account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7850,34 +7899,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Vai alle mie iscrizioni</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Vai ai miei video</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Vai alle mie importazioni</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Vai ai miei canali</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Impossibile recuperare le credenziali del Client OAuth: <x id="PH" equiv-text="error.text"/>. Assicurati di aver configurato correttamente PeerTube (config/ directory), in particolare la sezione "webserver".</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Devi riconnetterti.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Scorciatoie per la tastiera:</target>
@@ -7888,6 +7937,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -7911,38 +7966,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target>Nome utente o password non corretti.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Il tuo account è bloccato.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">qualsiasi lingua</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">nascondi</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">sfocatura</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">schermo</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Sconosciuto</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>La tua password è stata reimpostata con successo!</target>
@@ -9499,18 +9554,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>Troppi tentativi, si potrà provare di nuovo dopo <x id="PH"/> minuti.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Troppi tentativi, riprovare più tardi.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Errore del server.  Riprovare più tardi.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Iscrizione a tutti i canali correnti di <x id="PH"/>. Riceverai una notifica di tutti i loro nuovi video.</target>
@@ -10089,33 +10144,33 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target>Il video è stato caricato sul proprio account ed è privato.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>I dati associati (tag, descrizione, ...) saranno persi. Chiudere questa pagina ?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Il tuo video non è ancora caricato. Sei sicuro di volere chiudere questa pagina ?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Carica</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">Carica<x id="PH"/> </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Video pubblicato.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119">
@@ -10165,27 +10220,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Questo video non è disponibile su questa istanza. Vuoi essere reindirizzato sull'istanza di origine: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Redirezione</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Questo video contiene del contenuto sensibile. Sei sicuro di volerlo guardare?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Contenuto per adulti o esplicito</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Avanti il prossimo</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Annulla</target>
@@ -10195,62 +10250,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="translated">Autoplay sospeso</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Entra / esci dallo schermo intero (richiede focus player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Riproduci / Metti in pausa il video (richiede focus player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Disattiva / riattiva il video (richiede focus player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Passa a una percentuale del video: 0 è 0% e 9 è 90% (richiede il focus del player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Aumenta il volume (richiede il focus del player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Abbassa il volume (richiede il focus del player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Cerca il video in avanti (richiede focus player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Cerca il video all'indietro (richiede focus player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Aumenta la velocità di riproduzione (richiede focus del player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Diminuisci la velocità di riproduzione (richiede la messa a fuoco del player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Navigare nel video fotogramma per fotogramma (richiede focus player)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Mi piace</target>
index 2354692c8a16d2b53f3109a63b5744eb35782b65..942e10017dd8ccb08deea6e5a0a1cf765e99cb55 100644 (file)
       <trans-unit id="187187500641108332" datatype="html">
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="new">My watch history</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>作成</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="new"> The following link contains a private token and should not be shared with anyone. </target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">字幕</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>動画容量の制限</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>無制限 <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/>/ 日) <x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target state="translated">連合</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
       <trans-unit id="2159130950882492111">
         <source>Cancel</source>
         <target>キャンセル</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>このユーザーをBANする</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>ユーザー</target>
         <source>Username or email address</source>
         <target>ユーザー名かメールアドレス</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>パスワード</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">クリックしてパスワードをリセットします</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">パスワードを忘れました</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">あなたのアカウントでログインする事で、コンテンツを公開することができます</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>ログイン</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>パスワードをお忘れですか</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">申し訳ありません。インスタンス管理者がPeerTubeのメールシステムの設定をしていないため、パスワードを復元することができません。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">メールアドレスを入力すれば、パスワードをリセットするためのリンクが送信されます。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1164,26 +1182,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>メールアドレス</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>メールアドレス</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">リセット</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="new">on this instance</target>
@@ -1534,9 +1552,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>アカウントを作成する</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">自分の動画</target>
@@ -1603,10 +1621,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">動画</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1685,8 +1703,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">I am a teapot</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1779,8 +1797,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="needs-translation">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">グローバル検索</target>
@@ -2549,8 +2567,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="new">Upload on hold</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3241,11 +3259,7 @@ The link will expire within 1 hour.</source>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>状態</target>
@@ -3315,11 +3329,7 @@ The link will expire within 1 hour.</source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action }}"/> </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>ホスト</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3331,9 +3341,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">新しいタブでインスタンスを開く</target>
@@ -3344,13 +3354,13 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">あなたのインスタンスはまだ誰もフォローしていません。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3360,16 +3370,8 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">ドメインをフォロー</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">インスタンスをフォローする</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3419,11 +3421,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>ユーザー名</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">e.g.jane_doe</target>
@@ -3453,9 +3455,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>権限</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3480,15 +3482,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3745,6 +3741,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -4064,8 +4066,8 @@ The link will expire within 1 hour.</source>
         <target state="new">
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">ミュートしたドメイン</target>
@@ -6032,11 +6034,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">チャンネル</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">このアカウントはチャンネルを持ってません。</target>
@@ -6081,6 +6080,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6642,13 +6647,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="6979021199788941693">
         <source>Your message has been sent.</source>
         <target>メッセージを送信しました。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>このフォームに最近送信しています</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">このアカウントの動画</target>
@@ -6693,13 +6698,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">
           <x id="PH"/> direct account followers
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">アカウントを通報する</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
@@ -6709,31 +6714,21 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">ユーザー名をコピーしました</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">チャンネル登録者1人</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated">チャンネル登録者数 <x id="PH"/> 人</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">音声のみ</target>
@@ -6783,6 +6778,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target>自動 (FFmpeg 経由)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6931,18 +6932,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">ドメインが必要です。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">入力されたドメインは無効です。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>無制限</target>
@@ -7098,24 +7115,50 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> は無効です。
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>フォローリクエストを送信しました!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>本当に <x id="PH"/> のフォローを解除しますか?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>フォロー解除</target>
@@ -7124,8 +7167,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>あなたはもう <x id="PH"/> をフォローしていません。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>有効</target>
@@ -7597,9 +7640,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>エラー</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">標準のログ</target>
@@ -7640,16 +7683,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>ユーザーパスワードを更新する</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">フォローリスト</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">フォロワーリスト</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">User <x id="PH"/> updated.</target>
@@ -7685,16 +7720,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="translated">他インスタンスとの連合</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">フォローしているインスタンス</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">フォローされているインスタンス</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">動画が削除され、コメントも削除されます。</target>
@@ -7725,6 +7752,24 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>メールを確認済みとして設定</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
@@ -8029,13 +8074,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1137937154872046253">
         <source>Video channel <x id="PH"/> created.</source>
         <target>動画チャンネル <x id="PH"/> を作成しました。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>この名前は、このインスタンス上に既に存在します。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>動画チャンネル: <x id="PH"/> を更新しました。</target>
@@ -8056,11 +8101,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="translated">バナーが削除されました。</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>動画チャンネル ( <x id="PH"/>) の表示名を入力してください</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>動画チャンネル <x id="PH"/> を削除しました。</target>
@@ -8212,6 +8253,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target>所有権の変更リクエストが送信されました。</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8310,7 +8357,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>アカウントを購読する</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">プレイリスト</target>
@@ -8357,35 +8404,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>自分の登録チャンネルへ移動</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>動画一覧</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>インポートした動画一覧</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>マイチャンネルに移動する</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>再接続する必要があります。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>キーボードショートカット:</target>
@@ -8398,6 +8445,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8426,38 +8479,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>ユーザー名またはパスワードが違います。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">あなたのアカウントはブロックされてます。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">表示しない</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">ぼかす</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">表示する</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>パスワードは正常にリセットされました!</target>
@@ -10033,18 +10086,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>試行回数が多すぎます。 <x id="PH"/> 分後にもう一度お試しください。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>試行回数が多すぎます。しばらくしてからもう一度お試しください。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>サーバーエラーです。 後でもう一度やり直してください。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</target>
@@ -10638,33 +10691,33 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275">
         <source>Your video was uploaded to your account and is private.</source>
         <target>動画はこのアカウントに非公開でアップロードされています。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>関連するデータ (タグ、説明など) は失われます。このページから移動してもよろしいですか?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>動画はまだアップロードされていません。このページから移動してもよろしいですか?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">アップロード</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">アップロード <x id="PH"/> </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>動画が投稿されました。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>保存していない変更があります。 ページを移動すると、変更した内容は失われます。</target>
@@ -10731,28 +10784,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">リダイレクト</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>この動画には成人向けまたは過激なコンテンツが含まれています。本当に再生しますか?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>成人向けまたは過激なコンテンツ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">キャンセル</target>
@@ -10761,63 +10814,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">自動再生は停止中です</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">再生速度を早くする (アクティブのプレーヤーが必要)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">再生速度を遅くする (アクティブのプレーヤーが必要)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>高く評価</target>
index 7367a10caa7d25d093247a528273d5c76eeea9f4..7edbfaace5d99063029763bd1459e1fd810fd7eb 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source><target state="new">My watch history</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source><target state="new">subtitles</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source> Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="new">
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="new">Ban this user</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html</context><context context-type="linenumber">16</context></context-group></trans-unit><trans-unit id="7252854992688790751" datatype="html">
         <source> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit><trans-unit id="7215649348148521605" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7215649348148521605" datatype="html">
         <source> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>lo pilno</target>
         <source>Username or email address</source>
         <target state="new">Username or email address</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
+      </trans-unit>
       
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source><target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit><trans-unit id="2101170466365500913" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="2101170466365500913" datatype="html">
         <source> Logging into an account lets you publish content </source><target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>co'a cmisau</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>.i mi nalmo'i le mi lerpoijaspu</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
         <source> Enter your email address and we will send you a link to reset your password. </source><target state="new"> Enter your email address and we will send you a link to reset your password. </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</source><target state="new">An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</target>
@@ -1157,17 +1175,17 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>lo ve samymri</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source><target state="new">Reset</target>
         
         <note priority="1" from="description">Password reset button</note>
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       
       <trans-unit id="4319634264526091601" datatype="html">
@@ -1534,7 +1552,7 @@ galfi le mi japyvla</target>
         <source>Create an account</source>
         <target>zbasu lo pilno</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source><target state="new">My videos</target>
@@ -1580,7 +1598,7 @@ galfi le mi japyvla</target>
         <target state="new">VIDEOS</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="new">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1648,7 +1666,7 @@ galfi le mi japyvla</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/menu/notification.component.html</context><context context-type="linenumber">49</context></context-group></trans-unit><trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source><target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source><target state="new">That's an error.</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context>
@@ -1714,7 +1732,7 @@ galfi le mi japyvla</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context><context context-type="linenumber">42</context></context-group></trans-unit><trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source><target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2422,7 +2440,7 @@ galfi le mi japyvla</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3094,11 +3112,7 @@ galfi le mi japyvla</target>
         <target state="new">ID</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group></trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3180,11 +3194,7 @@ galfi le mi japyvla</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3195,7 +3205,7 @@ galfi le mi japyvla</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source><target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3207,12 +3217,12 @@ galfi le mi japyvla</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3222,14 +3232,7 @@ galfi le mi japyvla</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit><trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source><target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3279,7 +3282,7 @@ galfi le mi japyvla</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source><target state="new">e.g. jane_doe</target>
         
         <note priority="1" from="description">Username choice placeholder in the registration form</note>
@@ -3311,7 +3314,7 @@ galfi le mi japyvla</target>
         <target state="new">Role</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source> Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3334,15 +3337,9 @@ galfi le mi japyvla</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="new">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3569,7 +3566,13 @@ galfi le mi japyvla</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="4691552465058437520" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit><trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source><target state="new">Commented video</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">82</context></context-group></trans-unit><trans-unit id="7266085473379376028" datatype="html">
@@ -3893,7 +3896,7 @@ galfi le mi japyvla</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5708,11 +5711,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5750,7 +5750,13 @@ channel with the same name (<x id="PH_2"/>)!</source><target state="new">Do you
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6361,12 +6367,12 @@ zbasu lo pilno</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source><target state="new">Account videos</target>
         
@@ -6401,10 +6407,10 @@ zbasu lo pilno</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source><target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source><target state="new">VIDEOS</target>
@@ -6416,24 +6422,16 @@ zbasu lo pilno</target>
         <target state="new">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source><target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source><target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6481,7 +6479,13 @@ zbasu lo pilno</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="931255636742351800" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit><trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source><target state="new">No limit</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="5250062810079582285" datatype="html">
@@ -6600,17 +6604,33 @@ zbasu lo pilno</target>
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group></trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
+      </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="new">Unlimited</target>
@@ -6741,7 +6761,27 @@ zbasu lo pilno</target>
           <x id="PH"/> removed from instance followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source>
           <x id="PH"/> is not valid
@@ -6750,19 +6790,25 @@ zbasu lo pilno</target>
           <x id="PH"/> is not valid
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>.i mo'u mrilu lo jersi pe'a ve cpedu</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>co'u jersi pe'a</target>
@@ -6774,7 +6820,7 @@ zbasu lo pilno</target>
           <x id="PH"/> anymore.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7232,7 +7278,7 @@ zbasu lo pilno</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7270,13 +7316,7 @@ zbasu lo pilno</target>
         <source>Update user password</source>
         <target state="new">Update user password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit><trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source><target state="new">Following list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group></trans-unit><trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source><target state="new">Followers list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit>
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7307,13 +7347,7 @@ zbasu lo pilno</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/users.routes.ts</context><context context-type="linenumber">45</context></context-group></trans-unit><trans-unit id="8564701209009684429" datatype="html">
         <source>Federation</source><target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source><target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group></trans-unit><trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source><target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7341,7 +7375,25 @@ zbasu lo pilno</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -7646,12 +7698,12 @@ zbasu lo pilno</target>
           <x id="PH"/> .ly. noi vidvi te tivni
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>.i le cmene xa'o zasti ci'e le mupli</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>.i mo'u galfi la'o ly. 
@@ -7668,13 +7720,7 @@ zbasu lo pilno</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>.i mo'u vimcu la'o ly. 
@@ -7817,7 +7863,13 @@ zbasu lo pilno</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
         <target>lo mi te tivni</target>
@@ -7912,7 +7964,7 @@ zbasu lo pilno</target>
         <target>jersi pe'a le pilno</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7959,34 +8011,34 @@ zbasu lo pilno</target>
         <source>Go to my subscriptions</source>
         <target>klama lo se jersi pe'a be mi</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>klama lo mi vidvi</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>klama lo se nerbei be mi</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>klama lo mi te tivni</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source><target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       
       
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -7997,6 +8049,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8020,38 +8078,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>.i snada lo nu mo'u galfi le do japyvla</target>
@@ -9594,17 +9652,17 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/> minutes.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10107,20 +10165,20 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="new">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source><target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload 
           <x id="PH"/>
@@ -10129,13 +10187,13 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>.i lo se vidvi mo'u co'a gubni</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10199,26 +10257,26 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source><target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source><target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source><target state="new">Cancel</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">646</context></context-group></trans-unit>
@@ -10226,62 +10284,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>nu zanru lo se vidvi</target>
index bec074ca7b2eee5984b2ee9561fba4ad598c0c53..a982fd2a9b390d2ec24e13bf45a38105c77aed6a 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Azray-inu n tmeẓriwt</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3099741642167775297" datatype="html">
         <source>Download</source>
         <target>Sider</target>
         <source>video</source>
         <target>tavidyut</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">Aseɣwen-a deg-s ajuṭun sli, ur ilaq ara ad yettwabḍu ula d yiwen.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">iduzwilen</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="6325096236207614377" datatype="html">
         <source>Reason...</source>
         <target state="translated">Taɣẓint...</target>
       <trans-unit id="2602586221576511475" datatype="html">
         <source>Video quota</source>
         <target state="translated">Amur n tvidyut</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="translated">War tilas <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/>i wass)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <source>Federation</source>
         <target state="translated">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="translated">Ineḍfaren</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Tummant-a tsareg ajerred. Γas akken, ɣur-k·m senqed <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Tiwtilin<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Tiwtilin<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> send timerna n umiḍan. Tzemreḍ daɣen ad tnadiḍ tummant-nniḍen i wakken ad yemṣada swaswa wayen i tuḥwaǧeḍ deg: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Akka tura tummant-a ur tsirig ara ajerred n yiseqdacen, ilaq ad tesneqdeḍ <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Tiwtilin<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> i wugar n telqayt neɣ i tifin n tummant ara ak·am-imudden tazmert n ujerred i umiḍan, syen ad d-tsalayeḍ tividyutin din. Af-d ayla-k·m gar waṭas n tummanin yemgaraden deg: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729" datatype="html">
         <source>User</source>
         <target>Aseqdac</target>
         <source>Username or email address</source>
         <target>Isem n useqdac neɣ tansa imayl</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429" datatype="html">
         <source>Password</source>
         <target>Awal uffir</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Si da akken ad twenzeḍ awal uffir</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Ttuɣ awal-iw uffir</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Anekcum ɣer umiḍan ad ak·akem-yeǧǧ ad tsuffɣeḍ agbur</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966" datatype="html">
         <source>Login</source>
         <target>Tuqqna</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Or sign in with</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367" datatype="html">
         <source>Forgot your password</source>
         <target state="translated">Tettuḍ awal-ik uffir</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Suref-aɣ, ur tezmireḍ ara ad d-terreḍ awal-inek·inem uffir acku anedbal-ik·im n tummant ur isefrek ara anagraw n yimayl n PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Sekem tansa-inek·inem n yimayl syen nekkni ad ak·am-n-aznen aseɣwen i wakken ad twennzeḍ awal-ik·im uffir.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1147,26 +1165,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664" datatype="html">
         <source>Email</source>
         <target>Imayl</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610" datatype="html">
         <source>Email address</source>
         <target>Tansa email</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Wennez</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">deg tummant-a</target>
@@ -1507,7 +1525,7 @@ The link will expire within 1 hour.</source>
         <target>Rnu amiḍan</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="8936704404804793618" datatype="html">
         <source>Videos</source>
@@ -1538,7 +1556,7 @@ The link will expire within 1 hour.</source>
         <source>VIDEOS</source>
         <target state="translated">VIDEOS</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">Kter imahilen yemqaraben</target>
@@ -1657,7 +1675,7 @@ The link will expire within 1 hour.</source>
         <source>I'm a teapot</source>
         <target state="translated">Nekk d t·amsatay·t</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Tagi d tuccḍa.</target>
@@ -1750,8 +1768,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">midyat ɣezzif aṭas i uqeddac-a. Ma ulac aɣilif nermes anedbal-ik·im ma yella tebɣiḍ ad ternuḍ deg teɣzi n talast.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="2468689683507870964" datatype="html">
         <source>In this instance's network</source>
         <target state="translated">Deg uzeṭṭa n tummant-a</target>
@@ -2431,7 +2449,7 @@ The link will expire within 1 hour.</source>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Nesḥassef, tamahilt n usali tensa i umiḍan-ik·im. Ma yella tebɣiḍ ad ternuḍ tividyutin, anedbal yezmer ad yekkes asekker afmiḍi-inek·inem.</target>
@@ -2999,11 +3017,7 @@ The link will expire within 1 hour.</source>
         <target state="translated">Tavidyut/Awennit/Amiḍan</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">22</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="translated">Abagan n uneḍfar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="3301856295120048857" datatype="html">
         <source>State <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Addad n <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3167,11 +3181,7 @@ The link will expire within 1 hour.</source>
         <target state="translated">Ɛwed ajuṭun amynut</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-applications/my-account-applications.component.html</context><context context-type="linenumber">35</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target>Asneftaɣ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Tuɣalin tettusireg <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3181,7 +3191,7 @@ The link will expire within 1 hour.</source>
         <source>Unfollow</source>
         <target state="translated">Ḥbes aḍfar</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Ldi tummant deg yiccer amaynut</target>
@@ -3192,13 +3202,13 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Ulac inebgi yettwafen yemṣada d yimsizedgen imiranen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Tummant-ik·im ulac win i teṭṭafar.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Sken <x id="INTERPOLATION"/> i <x id="INTERPOLATION_1"/> n <x id="INTERPOLATION_2"/> yinebgawen</target>
@@ -3207,18 +3217,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Akka i d-yettban mačči ɣef uqeddac HTTPS i telliḍ. Aqeddac-ik·im web yesra ad yettwarmed TLS i uḍfar n yiqeddacen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Ḍfer taɣulin</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Ḍfer tummanin</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">Tigawt</target>
@@ -3341,11 +3343,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023" datatype="html">
         <source>Username</source>
         <target>Nom d'utilisateur</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">am. jane_doe</target>
@@ -3373,9 +3375,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target>Tamlilt</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Anigtengel yermed. Ableɣ n tvidyut yettaṭṭaf akan teɣzi <x id="START_TAG_STRONG"/>taneẓlit<x id="CLOSE_TAG_STRONG"/> n tvidyut. <x id="LINE_BREAK"/> Deg tuget, aseqdac-a yezmer ad d-isali ~ <x id="INTERPOLATION"/>. </target>
@@ -3392,15 +3394,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Izegrar n usesteb</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Ulac (alɣu adigan)</target>
@@ -3631,6 +3627,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -5465,6 +5467,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Tebɣiḍ s tidet ad tekkseḍ <x id="PH" equiv-text="videoChannel.displayName"/>? Ad yekkes <x id="PH_1" equiv-text="videoChannel.videosCount"/> tividyutin i d-yettwasulin deg ubadu-a, syen ur tettizmired ara ad ternuḍ abadu-nniḍen s yisem-nni kan (<x id="PH_2" equiv-text="videoChannel.name"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -5676,11 +5684,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">IBUDA</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">Amiḍan-a ulac ɣur-s ibuda n tvidyut.</target>
@@ -6179,12 +6184,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Your message has been sent.</source>
         <target state="translated">Izen-ik·im yettwazen.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="translated">Tuzneḍ yakan tiferkit-a</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Tividyutin n umiḍan</target>
@@ -6226,13 +6231,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="4856575356061361269" datatype="html">
         <source><x id="PH"/> direct account followers </source>
         <target state="translated"><x id="PH"/> ineḍfaren n umiḍan usriden </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Azen aneqqis ɣef umiḍan-a</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDEOS</target>
@@ -6242,29 +6247,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">Yettwanɣel yisem n useqdac</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 umulteɣ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> yimultaɣ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Tummanin i teṭṭafareḍ</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Tummanin i ak·akem-yeṭṭafaren</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="3008420115644088420" datatype="html">
         <source>Configuration</source>
         <target>Tawila</target>
@@ -6319,6 +6316,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target state="translated">Awurman (s ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6459,23 +6462,39 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Taɣult tettusra.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Tiɣula yettwaskecmen d tarimeɣta.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Tiɣula yettwaskecmen yella deg-sent uεiwed.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="translated"><x id="PH"/> d arameɣtu </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target>War talast</target>
@@ -6629,17 +6648,43 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source><x id="PH"/> removed from instance followers </source>
         <target state="translated"><x id="PH"/> yettwakkes sɣur ineḍfaren n tummant </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="translated">Asuter n uḍfar yettwazen!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="translated">Tebɣiḍ s tidet ad tḥebseḍ aḍfar n <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Ur ṭṭafarara</target>
@@ -6648,8 +6693,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926" datatype="html">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target state="translated">Ur teṭṭafareḍ <x id="PH"/> yiwen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target>irmed</target>
@@ -7188,9 +7233,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target>Tuccḍa</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Standard logs</target>
@@ -7231,16 +7276,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Tabdart n uḍfar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Tabdart n yineḍfaren</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Aseqdac <x id="PH"/> yettwaleqqem.</target>
@@ -7276,16 +7313,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Tummanin teṭṭafareḍ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Tummanin i ak·akem-yeṭṭafaren</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Tividyutin ad ttwakksent, iwenniten ad ttwasfesxen.</target>
@@ -7316,7 +7345,25 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Sbadu imayl am wakken yettusenqed</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="translated">Ur tezmired ara ad tgedleḍ aseqdac aẓar.</target>
@@ -7614,13 +7661,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1137937154872046253" datatype="html">
         <source>Video channel <x id="PH"/> created.</source>
         <target state="translated">Abadu n tvidyut <x id="PH"/> yettwarna.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="translated">Isem-a yella yakan ɣef tummant-a.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="translated">Abadu n tvidyut <x id="PH"/> yettwaleqqem.</target>
@@ -7641,11 +7688,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Aɣrrac yettwakkes.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="translated">Ttxil-k·m aru isem yettwaskanen n ubadu n tvidyut ( <x id="PH"/>) i usentem</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="translated">Abadu n tvidyut <x id="PH"/> yettwakkes.</target>
@@ -7790,6 +7833,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target state="translated">Ownership change request sent.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -7914,7 +7963,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Multeɣ ɣer umiḍan</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">TIBDARIN N TΓURI</target>
@@ -7961,34 +8010,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="translated">Ddu ɣer yimultaɣ-inu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="translated">Ddu ɣer tvidyutin-inu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="translated">Ddu ɣer waktaren-inu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="translated">Ddu ɣer yibuda-inu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">D awezɣi ad d-nerr inekcam n umsaɣ OAuth: <x id="PH" equiv-text="error.text"/>. Ḍmen belli tsewleḍ PeerTube akken iwata (akaram n uswel/), aṭas tigezmi n "aqeddac web".</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="translated">Tesriḍ ad teqqneḍ.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="translated">Inegzumen n unaswi:</target>
@@ -7999,6 +8048,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8021,38 +8076,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target>Isem n useqdac neɣ awal n uɛeddi d urameɣtu.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Amiḍan-ik·im yettusewḥel.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target>ffer</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">sken</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target>D arussin</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">yal tutlayt</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
@@ -9587,18 +9642,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070" datatype="html">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target state="translated">Aṭas n tikkal i tεerḍeḍ, ttxil-k·m εreḍ tikkelt-nniḍen seld <x id="PH"/> tesdatin.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="translated">Aṭas n yineεruḍen, ttxil-k·m εreḍ tikkelt-nniḍen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="translated">Tuccḍa deg uqeddac. Ttxil-k·m εreḍ tikkelt-nniḍen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Multeɣ ɣer meṛṛa ibuda imiranen n <x id="PH"/>. Ad d-teṭṭfeḍ ilɣa ɣef meṛṛa tividyutin timaynutin.</target>
@@ -10113,33 +10168,33 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="translated">Tavidyut-ik·im tettwasuli ɣer umiḍan-ik·im yerna d tusligt.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="translated">Maca ad tesruḥeḍ isefka yemcudden (tibzimin, aglam...), d tidet tebɣiḍ ad teffɣeḍ seg usebter-a?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="translated">Tavidyut-a mazal ur d-tettwasuli ara, d tidet tebɣiḍ ad teffɣeḍ seg usebter-a?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Sali</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">Sali-d <x id="PH"/> </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="translated">Tavidyut yettwasuffɣen.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10204,27 +10259,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Tavidyut-a ulac-itt deg tummant. Tebɣiḍ ad tettuwellheḍ ɣer tummant taneẓlit: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Allus n uwelleh</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="translated">Tavidyut-a deg-s agbur ai yimeqqranen neɣ agbur amḥulfu. D tidet tebɣiḍ ad t-twaliḍ?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="translated">Agbur i yimeqqranen neq agbur amḥulfu</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Uḍfir</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Sefsex</target>
@@ -10234,62 +10289,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="translated">Taɣuri tawurmant tettwaseḥbes</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Kcem/ffeɣ askar n ugdil aččuran (yesra afukus ɣef yimeɣri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Taɣuri/Aḥbas n tvidyutPause(yesra afukus n tɣuri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Sgugem/kkes asgugem tavidyut (yesra afukus n tɣuri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Ɛeddi ɣer ufmiḍi n tvidyut: 0 d 0% akked 9 d 90% (yesra afukus n tɣuri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Snerni ablaɣ (yesra afukus n tɣuri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Senqes ableɣ (yesra afukus n tɣuri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Seddu tavidyut ɣer sdat (yesra afukus n tɣuri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Err tavidyut ɣer deffir (yesra afukus n tɣuri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Snerni arured n tɣuri (yesra afukus n tɣuri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Senqes arured n tɣuri (yesra afukus n tɣuri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Inig deg tvidyut tugna s tugna (yesra afukus ɣef yimeɣri)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="translated">Teεǧeb-iyi tavidyut</target>
index d02c433ac348102c6f27ef06f3f328674bdcb820..af024c47c16716a182c7934206b0db294f090846 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="new">My watch history</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">동영상</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>영상 업로드 제한</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>이 사용자 강퇴</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>사용자</target>
         <source>Username or email address</source>
         <target>사용자명 혹은 이메일 주소</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>암호</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>로그인</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>암호 잊음</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1152,26 +1170,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>이메일</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>이메일 주소</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
@@ -1544,7 +1562,7 @@ The link will expire within 1 hour.</target>
         <target>계정 만들기</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1601,7 +1619,7 @@ The link will expire within 1 hour.</target>
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1681,7 +1699,7 @@ The link will expire within 1 hour.</target>
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1765,8 +1783,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2481,7 +2499,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3228,11 +3246,7 @@ The link will expire within 1 hour.</target>
         <target state="new">ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3307,11 +3321,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3324,7 +3334,7 @@ The link will expire within 1 hour.</target>
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3336,12 +3346,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3351,16 +3361,8 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3407,11 +3409,11 @@ The link will expire within 1 hour.</target>
       <trans-unit id="5248717555542428023" datatype="html">
         <source>Username</source>
         <target state="new">Username</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3441,9 +3443,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target state="new">Role</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3468,15 +3470,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3735,7 +3731,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target state="new">Commented video</target>
@@ -4064,7 +4066,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -6026,11 +6028,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -6070,7 +6069,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6672,12 +6677,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
@@ -6727,12 +6732,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       
       <trans-unit id="1504521795586863905" datatype="html">
@@ -6747,27 +6752,19 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6817,6 +6814,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6959,18 +6962,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="new">Unlimited</target>
@@ -7130,26 +7149,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="new">
           <x id="PH"/> is not valid
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
@@ -7160,8 +7205,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">You are not following 
           <x id="PH"/> anymore.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7628,9 +7673,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="new">Error</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7675,16 +7720,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7724,16 +7761,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7764,7 +7793,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -8076,12 +8123,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> created.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="new">Video channel 
@@ -8098,13 +8145,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="new">Video channel 
@@ -8267,6 +8308,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8372,7 +8419,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -8418,35 +8465,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -8457,6 +8504,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8480,38 +8533,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="new">Your password has been successfully reset!</target>
@@ -10097,18 +10150,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target state="new">Too many attempts, please try again after 
           <x id="PH"/> minutes.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10702,35 +10755,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="new">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="new">Upload 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="new">Video published.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10780,27 +10833,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -10810,62 +10863,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index 6c94a9e35512c17a12da6fae8d867bb37f02f516..1c1a2860b13ea98320e054b4fceff7a762909a87 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source><target state="new">My watch history</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source><target state="new">subtitles</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source> Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="new">
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="new">Ban this user</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html</context><context context-type="linenumber">16</context></context-group></trans-unit><trans-unit id="7252854992688790751" datatype="html">
         <source> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit><trans-unit id="7215649348148521605" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7215649348148521605" datatype="html">
         <source> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729" datatype="html">
         <source>User</source>
         <target state="new">User</target>
         <source>Username or email address</source>
         <target state="new">Username or email address</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
+      </trans-unit>
       
       <trans-unit id="1431416938026210429" datatype="html">
         <source>Password</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source><target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit><trans-unit id="2101170466365500913" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="2101170466365500913" datatype="html">
         <source> Logging into an account lets you publish content </source><target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966" datatype="html">
         <source>Login</source>
         <target state="new">Login</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367" datatype="html">
         <source>Forgot your password</source>
         <target state="new">Forgot your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
         <source> Enter your email address and we will send you a link to reset your password. </source><target state="new"> Enter your email address and we will send you a link to reset your password. </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</source><target state="new">An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</target>
@@ -1131,17 +1149,17 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610" datatype="html">
         <source>Email address</source>
         <target state="new">Email address</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source><target state="new">Reset</target>
         
         <note priority="1" from="description">Password reset button</note>
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       
       <trans-unit id="4319634264526091601" datatype="html">
@@ -1503,7 +1521,7 @@ The link will expire within 1 hour.</target>
         <source>Create an account</source>
         <target state="new">Create an account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source><target state="new">My videos</target>
@@ -1549,7 +1567,7 @@ The link will expire within 1 hour.</target>
         <target state="new">VIDEOS</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="new">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1617,7 +1635,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/menu/notification.component.html</context><context context-type="linenumber">49</context></context-group></trans-unit><trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source><target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source><target state="new">That's an error.</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context>
@@ -1683,7 +1701,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context><context context-type="linenumber">42</context></context-group></trans-unit><trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source><target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2377,7 +2395,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3031,11 +3049,7 @@ The link will expire within 1 hour.</target>
         <target state="new">ID</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group></trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3117,11 +3131,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3132,7 +3142,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source><target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3144,12 +3154,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3159,14 +3169,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit><trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source><target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3216,7 +3219,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source><target state="new">e.g. jane_doe</target>
         
         <note priority="1" from="description">Username choice placeholder in the registration form</note>
@@ -3248,7 +3251,7 @@ The link will expire within 1 hour.</target>
         <target state="new">Role</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source> Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3271,15 +3274,9 @@ The link will expire within 1 hour.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="new">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3504,7 +3501,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="4691552465058437520" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit><trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source><target state="new">Commented video</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">82</context></context-group></trans-unit><trans-unit id="7266085473379376028" datatype="html">
@@ -3826,7 +3829,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5631,11 +5634,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5673,7 +5673,13 @@ channel with the same name (<x id="PH_2"/>)!</source><target state="new">Do you
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6263,12 +6269,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source><target state="new">Account videos</target>
         
@@ -6303,10 +6309,10 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source><target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source><target state="new">VIDEOS</target>
@@ -6318,24 +6324,16 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source><target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source><target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6383,7 +6381,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="931255636742351800" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit><trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source><target state="new">No limit</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="5250062810079582285" datatype="html">
@@ -6502,17 +6506,33 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group></trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
+      </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="new">Unlimited</target>
@@ -6643,7 +6663,27 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source>
           <x id="PH"/> is not valid
@@ -6652,19 +6692,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> is not valid
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
@@ -6676,7 +6722,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> anymore.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7134,7 +7180,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7172,13 +7218,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Update user password</source>
         <target state="new">Update user password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit><trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source><target state="new">Following list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group></trans-unit><trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source><target state="new">Followers list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit>
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7209,13 +7249,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/users.routes.ts</context><context context-type="linenumber">45</context></context-group></trans-unit><trans-unit id="8564701209009684429" datatype="html">
         <source>Federation</source><target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source><target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group></trans-unit><trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source><target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7243,7 +7277,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -7548,12 +7600,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> created.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="new">Video channel 
@@ -7570,13 +7622,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="new">Video channel 
@@ -7719,7 +7765,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
         <target state="new">My channels</target>
@@ -7814,7 +7866,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7861,34 +7913,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source><target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       
       
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -7899,6 +7951,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -7922,38 +7980,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="new">Your password has been successfully reset!</target>
@@ -9497,17 +9555,17 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/> minutes.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10005,20 +10063,20 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="new">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source><target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload 
           <x id="PH"/>
@@ -10027,13 +10085,13 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="new">Video published.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10096,26 +10154,26 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source><target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source><target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source><target state="new">Cancel</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">646</context></context-group></trans-unit>
@@ -10123,62 +10181,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index 494d4c982b3553d8a05fec56cb8bc32c76a9799f..b02226725eb96f3c88a2ce448e486a911c3dde44 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Min seer historikk</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">undertekster</target>
       <trans-unit id="2602586221576511475" datatype="html">
         <source>Video quota</source>
         <target state="new">Video quota</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="new">
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="new">Ban this user</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729" datatype="html">
         <source>User</source>
         <target state="new">User</target>
         <source>Username or email address</source>
         <target state="new">Username or email address</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429" datatype="html">
         <source>Password</source>
         <target state="new">Password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit><trans-unit id="892063502898494584" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source><target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966" datatype="html">
         <source>Login</source>
         <target state="new">Login</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367" datatype="html">
         <source>Forgot your password</source>
         <target state="new">Forgot your password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1018,26 +1036,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664" datatype="html">
         <source>Email</source>
         <target state="new">Email</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610" datatype="html">
         <source>Email address</source>
         <target state="new">Email address</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
@@ -1410,7 +1428,7 @@ The link will expire within 1 hour.</target>
         <target state="new">Create an account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1467,7 +1485,7 @@ The link will expire within 1 hour.</target>
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="new">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1542,7 +1560,7 @@ The link will expire within 1 hour.</target>
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1623,8 +1641,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2340,7 +2358,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3068,11 +3086,7 @@ The link will expire within 1 hour.</target>
         <target state="new">ID</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group></trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3147,11 +3161,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3164,7 +3174,7 @@ The link will expire within 1 hour.</target>
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3176,12 +3186,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3191,16 +3201,8 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3250,7 +3252,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3280,9 +3282,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target state="new">Role</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3305,15 +3307,9 @@ The link will expire within 1 hour.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="new">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3570,7 +3566,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target state="new">Commented video</target>
@@ -3899,7 +3901,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5818,11 +5820,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5862,7 +5861,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6465,12 +6470,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
@@ -6517,12 +6522,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
@@ -6536,27 +6541,19 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6606,7 +6603,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
         <target state="new">No limit</target>
@@ -6738,18 +6741,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="new">Unlimited</target>
@@ -6899,26 +6918,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="new">
           <x id="PH"/> is not valid
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
@@ -6929,8 +6974,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">You are not following 
           <x id="PH"/> anymore.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7397,9 +7442,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="new">Error</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7444,16 +7489,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7493,16 +7530,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7533,7 +7562,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -7842,12 +7889,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> created.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="new">Video channel 
@@ -7864,13 +7911,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="new">Video channel 
@@ -8027,6 +8068,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8132,7 +8179,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -8178,35 +8225,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -8217,6 +8264,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8240,38 +8293,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="new">Your password has been successfully reset!</target>
@@ -9832,18 +9885,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target state="new">Too many attempts, please try again after 
           <x id="PH"/> minutes.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10438,35 +10491,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="new">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="new">Upload 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="new">Video published.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10534,27 +10587,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -10564,62 +10617,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index 31726555576160150886168021b960f7f351f65a..ea82323b4f9c05065d3711f6f1a8488234b0de38 100644 (file)
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source><target state="new">My watch history</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">ondertitels</target>
@@ -683,10 +683,10 @@ Annuleren</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Videoquotum</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Oneindig <x id="START_TAG_NG-CONTAINER" ctype="x-ng-container" equiv-text="&lt;ng-container>"/>( <x id="INTERPOLATION" equiv-text="{{ dailyUserVideoQuota | bytes: 0 }}"/> per dag) <x id="CLOSE_TAG_NG-CONTAINER" ctype="x-ng-container" equiv-text="&lt;/ng-container>"/></target>
@@ -765,7 +765,31 @@ Annuleren</target>
         <source>Federation</source>
         <target state="translated">Federatie</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="translated">volgers</target>
@@ -839,7 +863,7 @@ Een verbannen gebruiker kan niet langer inloggen.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Verban deze gebruiker</target>
@@ -904,19 +928,13 @@ Aanmelden</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Gebruiker</target>
@@ -927,64 +945,64 @@ Aanmelden</target>
         <source>Username or email address</source>
         <target>Gebruikersnaam of e-mailadres</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Wachtwoord</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Hier klikken om je wachtwoord opnieuw in te stellen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit><trans-unit id="892063502898494584" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source><target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Door in te loggen op een account, kunt u content publiceren</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Aanmelden</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Of meld je aan met</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Jouw wachtwoord vergeten</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Het spijt ons, maar we kunnen je wachtwoord niet herstellen. De beheerder van je exemplaar van PeerTube heeft het PeerTube-emailsysteem niet ingesteld.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Je email-adres invoeren en je krijgt van ons instructies om je wachtwoord opnieuw in te stellen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -994,26 +1012,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>E-mail</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>E-mailadres</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Herinstellen</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
@@ -1337,7 +1355,7 @@ Geen resultaten gevonden</target>
         <target>Account maken</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1394,7 +1412,7 @@ Geen resultaten gevonden</target>
         <source>VIDEOS</source>
         <target state="translated">VIDEO'S</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="new">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1469,7 +1487,7 @@ Geen resultaten gevonden</target>
         <source>I'm a teapot</source>
         <target state="translated">Ik ben een theepot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Dat is een fout.</target>
@@ -1550,8 +1568,8 @@ Geen resultaten gevonden</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">Media te groot voor de server. Gelieve je beheerder te contacteren als je de groottelimiet wil verhogen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2239,7 +2257,7 @@ Gefeliciteerd, de video achter
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Sorry, uploaden is uitgeschakeld voor je account. Als je een video wil toevoegen, dan moet een beheerder je quotum ontgrendelen.</target>
@@ -2951,11 +2969,7 @@ Je kan nu al informatie toevoegen over deze video.
         <target>ID</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group></trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target>Naam volger</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Status</target>
@@ -3021,11 +3035,7 @@ Je kan nu al informatie toevoegen over deze video.
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action }}"/> </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Surpluskopie toegelaten <x id="START_TAG_P-SORTICON" ctype="x-p-sortIcon" equiv-text="&lt;p-sortIcon>"/> <x id="CLOSE_TAG_P-SORTICON" ctype="x-p-sortIcon" equiv-text="&lt;/p-sortIcon>"/></target>
@@ -3033,7 +3043,7 @@ Je kan nu al informatie toevoegen over deze video.
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source><target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Exemplaar van PeerTube openen in nieuwe tab</target>
@@ -3045,25 +3055,18 @@ Je kan nu al informatie toevoegen over deze video.
         <source>No host found matching current filters.</source>
         <target state="translated">Geen host gevonden op basis van huidige filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Je exemplaar van PeerTube volgt niemand.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Nu te zien: <x id="INTERPOLATION" equiv-text="{{'{first}'}}"/> tot <x id="INTERPOLATION_1" equiv-text="{{'{last}'}}"/> van <x id="INTERPOLATION_2" equiv-text="{{'{totalRecords}'}}"/> hosts</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Domeinen volgen</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit><trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source><target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3113,7 +3116,7 @@ Je kan nu al informatie toevoegen over deze video.
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">vb. piet_fluwijn</target>
@@ -3141,9 +3144,9 @@ Je kan nu al informatie toevoegen over deze video.
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Rol</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Transcoding is ingeschakeld. De videoquota houden enkel rekening met de grootte van de <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong>"/>originele <x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong>"/> video. <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br/>"/> Deze gebruiker kan maximaal ~ <x id="INTERPOLATION" equiv-text="{{ computeQuotaWithTranscoding() | bytes: 0 }}"/> uploaden. </target>
@@ -3158,15 +3161,9 @@ Je kan nu al informatie toevoegen over deze video.
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="new">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3409,7 +3406,13 @@ Je kan nu al informatie toevoegen over deze video.
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target state="translated">Video met reacties</target>
@@ -3698,7 +3701,7 @@ Je kan nu al informatie toevoegen over deze video.
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Het lijkt dat je niet op een HTTPS-server zit. Om een andere server te volgen is TLS op jouw webserver vereist.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Domeinen dempen</target>
@@ -5571,11 +5574,8 @@ Vraag e-mail voor accountverificatie aan</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
@@ -5609,7 +5609,13 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Wil je echt <x id="PH"/> verwijderen? Dat verwijdert <x id="PH_1"/> video's die in dit kanaal geüpload zijn. Je kan ook geen nieuw kanaal meer maken met dezelfde naam. (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="translated">Mijn Kanalen</target>
@@ -6125,12 +6131,12 @@ Account aanmaken</target>
         <source>Your message has been sent.</source>
         <target>Jouw bericht is verstuurd.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>U hebt dit formulier onlangs al verzonden</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
@@ -6173,12 +6179,12 @@ Account aanmaken</target>
         <source><x id="PH"/> direct account followers </source>
         <target state="translated"><x id="PH"/> directe accountvolgers </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Deze account melden</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
@@ -6192,27 +6198,19 @@ Account aanmaken</target>
         <target>Gebruikersnaam gekopieerd</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 abonnee</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> abonnees</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Exemplaren van PeerTube die je volgt</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Exemplaren van PeerTube die je volgen</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Enkel audio</target>
@@ -6260,7 +6258,13 @@ Account aanmaken</target>
         <source>Auto (via ffmpeg)</source>
         <target>Automatisch (via ffmpeg)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
         <target state="translated">Onbeperkt</target>
@@ -6390,18 +6394,34 @@ Account aanmaken</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Domein is vereist.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">De ingevoerde domeinen zijn ongeldig.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">De ingevoerde domeinen bevatten dubbels.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Oneindig</target>
@@ -6555,24 +6575,50 @@ Account aanmaken</target>
         <source><x id="PH"/> removed from instance followers </source>
         <target><x id="PH"/> verwijderd uit volgers van dit exemplaar van PeerTube </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> is niet valide
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Volgverzoek(en) verstuurd!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Wil je echt <x id="PH"/> niet meer volgen?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Onvolgen</target>
@@ -6581,8 +6627,8 @@ Account aanmaken</target>
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>Je volgt <x id="PH"/> niet meer.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>ingeschakeld</target>
@@ -7030,9 +7076,9 @@ Account aanmaken</target>
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Fout</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Standaardlogboeken</target>
@@ -7073,16 +7119,8 @@ Account aanmaken</target>
         <target>Update gebruikerswachtwoord</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Lijst van gevolgden</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Lijst van volgers</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Gebruiker <x id="PH"/> bijgewerkt.</target>
@@ -7118,16 +7156,8 @@ Account aanmaken</target>
         <target state="translated">Federatie</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Exemplaren van PeerTube die je volgt</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Exemplaren van PeerTube die jou volgen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Videos zullen worden verwijderd, reacties als verwijderd gemarkeerd.</target>
@@ -7158,7 +7188,25 @@ Account aanmaken</target>
         <target>Zet E-mail als Geverifieerd</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
         <target>Je kan root niet verbannen.</target>
@@ -7450,12 +7498,12 @@ Account aanmaken</target>
         <source>Video channel <x id="PH"/> created.</source>
         <target>Videokanaal <x id="PH"/> gemaakt.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Deze naam bestaat al op dit exemplaar van PeerTube.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Videokanaal <x id="PH"/> bijgewerkt.</target>
@@ -7470,11 +7518,7 @@ Account aanmaken</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Typ alsjeblieft de weergavenaam van het videokanaal ( <x id="PH"/>) om te bevestigen</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Videokanaal <x id="PH"/> verwijderd.</target>
@@ -7613,6 +7657,12 @@ Account aanmaken</target>
         <source>Ownership change request sent.</source>
         <target>Eigenaarsveranderingsaanvrag gestuurd.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -7713,7 +7763,7 @@ Account aanmaken</target>
         <target>Abonneren op account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7759,34 +7809,34 @@ Account aanmaken</target>
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Naar mijn abonnementen gaan</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Ga naar mijn videos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Ga naar mijn imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Ga naar mijn kanalen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Kan OAuth Client-aanmeldinformatie niet ophalen: <x id="PH"/>. Vergewis je ervan dat je PeerTube (config/ map) juist hebt geconfigureerd, in het bijzonder het onderdeel "webserver".</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Je moet opnieuw verbinden.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Sneltoetsen:</target>
@@ -7797,6 +7847,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -7820,38 +7876,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target>Incorrecte gebruikersnaam of wachtwoord.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Je account is geblokkeerd.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">gelijk welke taal</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">verbergen</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">vervagen</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">weergeven</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Niet gekend</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Jouw wachtwoord is succesvol gereset!</target>
@@ -9379,18 +9435,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>Te veel pogingen. Probeer alstublieft opnieuw na <x id="PH"/> minuten.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Te vaak geprobeerd, probeer alstublieft later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Serverfout. Probeer later alstublieft weer.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Geabonneerd op alle huidige kanalen van <x id="PH"/>. U krijgt meldingen van al zijn of haar nieuwe video's.</target>
@@ -9963,35 +10019,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target>Jouw video is geupload naar jouw account en is privé.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Maar geassocieerde data(tags, beschrijving...) zullen verloren raken, weet je zeker dat je deze pagina wilt verlaten?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Jouw video is nog niet geupload, weet je zeker dat je deze pagina wilt verlaten?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Uploaden</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload <x id="PH"/> </source>
         <target>
           <x id="PH"/> uploaden
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Video gepubliceerd.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119">
@@ -10039,27 +10095,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Deze video is niet beschikbaar op dit exemplaar van PeerTube. Wil je doorverwezen worden naar het originele exemplaar &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Doorverwijzing</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Deze video bevat volwassen of expliciete inhoud. Weet je zeker dat je hem wilt kijken?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Volwassen of expliciete content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Volgende</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Annuleren</target>
@@ -10069,62 +10125,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="translated">Automatisch afspelen is opgeschort</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Volledig scherm ingaan/uitgaan (vereist focus op afspeler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">De video afspelen/pauseren (vereist focus op de afspeler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">De video dempen of niet meer dempen (vereist focus op de speler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Naar een percentage van de video springen: 0 is 0% and 9 is 90% (vereist focus op de speler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Het volume verhogen (vereist focus op de speler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Het volume verlagen (vereist focus op de speler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Vooruitspringen in de video (vereist focus op de speler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Achteruit springen in de video (vereist focus op de speler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Afspelen versnellen (vereist focus op de speler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Afspelen vertragen (vereist focus op de speler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Frame per frame door de video navigeren (vereist focus op de speler)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Like de video</target>
index f1dc9a13deb5cca7b46dec7747cbe421dbb097e1..d3cc91167570d9f680793a1f63f9b9f9189aa931 100644 (file)
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="new">My watch history</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Crear</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">vidèo</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="new">subtitles</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Quòta vidèo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>
         <target>Federacion</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106">
         <source>followers</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Fòrabandir aqueste utilizaire</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Utilizaire</target>
         <source>Username or email address</source>
         <target>Nom d’utilizaire o adreça electronica</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Senhal</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Clicatz aquí per reïnicializar vòstre senhal</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Connexion</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Senhal oblidat</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">O planhèm, podètz pas restaurar lo senhal perque l'administrator de l'instància configurèt pas lo sistèm de corrièl de PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1195,26 +1213,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Corrièl</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Adreça de corrièl</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="new">on this instance</target>
@@ -1581,7 +1599,7 @@ The link will expire within 1 hour.</target>
         <target>Crear un compte</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1649,7 +1667,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1727,8 +1745,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1821,8 +1839,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2588,7 +2606,7 @@ The link will expire within 1 hour.</target>
         <source>Upload on hold</source>
         <target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3314,11 +3332,7 @@ The link will expire within 1 hour.</target>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target>Gestion dels seguidors</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Estatisticas</target>
@@ -3393,11 +3407,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Òst</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3409,9 +3419,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3422,13 +3432,13 @@ The link will expire within 1 hour.</target>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3438,16 +3448,8 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3497,11 +3499,11 @@ The link will expire within 1 hour.</target>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Nom d’utilizaire</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3531,9 +3533,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Ròtle</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3558,15 +3560,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3831,6 +3827,12 @@ The link will expire within 1 hour.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -4171,8 +4173,8 @@ The link will expire within 1 hour.</target>
         <target state="new">
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -6176,11 +6178,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
         <target>Aqueste compte a pas cap de cadena.</target>
@@ -6225,6 +6224,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6817,12 +6822,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target>Messatge enviat.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Avètz ja enviat aqueste formulari fa pas gaire</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="new">Account videos</target>
@@ -6868,13 +6873,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">
           <x id="PH"/> direct account followers
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
@@ -6884,31 +6889,21 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="25349740244798533">
         <source>Username copied</source>
         <target>Nom d’utilizaire copiat</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Las instàncias que seguissètz</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Las instàncias que vos seguisson</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Àudio solament</target>
@@ -6958,6 +6953,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target>Auto (via ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -7108,18 +7109,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Un domeni es requerit.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Los domenis picats son pas valids.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Cap de limit</target>
@@ -7279,26 +7296,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> tirat dels seguidors de l’instància
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> es pas valid
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Demanda(s) de seguiment enviada !</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Volètz vertadièrament quitar de seguir 
           <x id="PH"/> ?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Quitar de seguir</target>
@@ -7309,8 +7352,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Seguèt pas mai 
           <x id="PH"/>.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>activada</target>
@@ -7801,9 +7844,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Error</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7848,16 +7891,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Actualizar lo senhal de l’utilizaire</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7897,16 +7932,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7937,7 +7964,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Passar l’adreça coma verificada</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
         <target>Podètz pas fòrabandir lo root.</target>
@@ -8250,13 +8295,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Cadena vidèo 
           <x id="PH"/> creada.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Aqueste nom existís ja sus aquesta instància.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Cadena vidèo 
@@ -8279,13 +8324,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Volgatz ben picar lo nom public de la cadena vidèo (
-          <x id="PH"/>) per dire de confirmar
-        </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Cadena vidèo 
@@ -8451,6 +8490,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target>Demanda de cambiament de proprietat enviada.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8553,7 +8598,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>S’abonar al compte</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="new">PLAYLISTS</target>
@@ -8600,35 +8645,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Anar a mos abonaments</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Anar a mas vidèos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Anar a mos imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Anar a ma cadena</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Vos cal vos reconnectar.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Acorchis clavièr :</target>
@@ -8639,6 +8684,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8665,39 +8716,39 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>Nom d’utilizaire o senhal incorrècte.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">tota lenga</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">amagar</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">mostrar</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Vòstre senhal es estat corrèctament reïnicializat !</target>
@@ -10302,18 +10353,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target>Tròp d’ensages, mercés de tornar ensajar dins 
           <x id="PH"/> minutas.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Tròp d’ensages, mercés de tornar ensajar mai tard.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Error servidor. Mercés de tornar ensajar mai tard.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10912,34 +10963,34 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target>La vidèo es estada enviada a vòstre compte e es privada.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Mas las donadas associadas (etiquetas, descripcion...) seràn perdudas, volètz vertadièrament quitar la pagina ?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>La vidèo es pas encara complètament enviada, volètz vertadièrament quitar la pagina ?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload <x id="PH"/> </source>
         <target>Enviar 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Vidèo publicada.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>Avètz de modificacions pas enregistradas. Se partissètz vòstras modificacions seràn perdudas. </target>
@@ -10987,27 +11038,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Aquesta vidèo conten un contengut per adult o explicite. Volètz vertadièrament la veire ?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Contengut per adult o explicite</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Seguent</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -11017,62 +11068,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="translated">La lectura automatica es suspenduda</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Aimar la vidèo</target>
index e5421bec89baa70733b22c4a66f68ad07df3b35a..8b02e562cfed5034e297a535b7f9064034398f05 100644 (file)
         <target state="translated">
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Moja historia oglądania</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Utwórz</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">film</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">Następujący odnośnik zawiera prywatny token i nie należy się nim z nikim dzielić.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">Twoja powierzchnia na filmy została przekroczona przez ten film (rozmiar filmu: <x id="PH" equiv-text="videoSizeBytes"/>, wykorzystano: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, powierzchnia: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">Twoja dzienna powierzchnia na filmy została przekroczona przez ten film (rozmiar filmu: <x id="PH" equiv-text="videoSizeBytes"/>, wykorzystano: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, powierzchnia <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">napisy</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Przestrzeń na filmy</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Nieograniczony 
         <target state="translated">Federacja</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
       <trans-unit id="2159130950882492111">
         <source>Cancel</source>
         <target>Anuluj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="translated">Zbanuj tego użytkownika</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Ta knstancja pozwala na rejestrację. Pamiętaj jednak sprawdzić <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Zasady<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Zasady<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> przed utworzeniem konta. Możesz też odnaleźć inną instancję spełniającą Twoje oczekiwania na: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/pl/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/pl/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Obecnie ta instancja nie pozwala na rejestrację użytkowników, możesz sprawdzić <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Zasady<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>, aby znaleźć więcej szczegółów lub znaleźć instancję, która pozwoli Ci na rejestrację konta i wysyłanie własnych filmów. Znajdź swoją spośród wielu instancji na: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/pl/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/pl/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Użytkownik</target>
         <source>Username or email address</source>
         <target>Nazwa użytkownika lub adres e-mail</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Hasło</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Kliknij tutaj aby zresetować swoje hasło</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Nie pamiętam hasła</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Logowanie na konto pozwala na publikację treści</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Zaloguj się</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Lub zaloguj się z</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Zapomniałem hasła</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Przepraszamy, nie możesz odzyskać hasła ponieważ administrator twojej instancji nie skonfigurował systemu e-mail.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Wprowadź swój adres e-mail, a my wyślemy link pozwalający na zresetowanie hasła.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1162,26 +1180,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>E-mail</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Adres e-mail</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Resetuj</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">na tej instancji</target>
@@ -1532,9 +1550,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>Utwórz konto</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">Moje filmy</target>
@@ -1601,10 +1619,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">FILMY</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">Równoczesne zadania importu</target>
@@ -1683,8 +1701,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">Jestem czajniczkiem</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">To błąd.</target>
@@ -1777,8 +1795,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">Zawartość multimedialna jest zbyt wielka dla tego serwerami skontaktuj się z administratorem, jeżeli chcesz aby zwiększył limit rozmiaru.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">WYSZUKIWANIE OGÓLNE</target>
@@ -2520,8 +2538,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="new">Upload on hold</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Niestety, możliwość wysyłania jest wyłączona dla Twojego konta. Jeżeli chcesz dodać filmy, administrator musi odblokować Twój przydział powierzchni.</target>
@@ -3203,11 +3221,7 @@ The link will expire within 1 hour.</source>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="translated">Nazwa obserwującego użytkownika</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target state="translated">Stan</target>
@@ -3276,11 +3290,7 @@ The link will expire within 1 hour.</source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action }}"/> </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Host</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Redundancja zezwolona 
@@ -3292,9 +3302,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Przestań obserwować</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Otwórz instancję w nowej karcie</target>
@@ -3305,28 +3315,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Brak hostów spełniających obecne kryteria.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Twoja instancja nie obserwuje nikogo.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Wyświetlanie <x id="INTERPOLATION"/> do <x id="INTERPOLATION_1"/> z <x id="INTERPOLATION_2"/> hostów</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Obserwuj domeny</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Zaobserwuj instancje</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">Działanie</target>
@@ -3376,11 +3378,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Nazwa użytkownika</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">np. jane_doe</target>
@@ -3408,9 +3410,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Rola</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Transkodowanie jest włączone. Limit użytej powierzchni bierze pod uwagę tylko <x id="START_TAG_STRONG"/>oryginalny<x id="CLOSE_TAG_STRONG"/> rozmiar filmów. <x id="LINE_BREAK"/> Maksymalnie, użytkownik może wysłać~ <x id="INTERPOLATION"/>. </target>
@@ -3427,15 +3429,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Wtyczka uwierzytelniania</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Brak (lokalne uwierzytelnianie)</target>
@@ -3689,6 +3685,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3997,8 +3999,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Wygląda na to, że nie jesteś na serwerze HTTPS. Twój serwer musi mieć aktywne TLS, aby obserwować inne serwery.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Wycisz domeny</target>
@@ -5959,11 +5961,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">KANAŁY</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">To konto nie ma kanałów.</target>
@@ -6002,6 +6001,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Czy na pewno cchesz usunąć <x id="PH" equiv-text="videoChannel.displayName"/>? Usuniesz w ten sposób <x id="PH_1" equiv-text="videoChannel.videosCount"/> filmów wysłanych na ten kanał i nie będziesz miał(a) możliwości założenia kanału o tej samej nazwie (<x id="PH_2" equiv-text="videoChannel.name"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6535,13 +6540,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="6979021199788941693">
         <source>Your message has been sent.</source>
         <target>Twoja wiadomość została wysłana.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="translated">Niedawno wypełniłeś(-aś) ten formularz</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Filmy konta</target>
@@ -6588,13 +6593,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">
           <x id="PH"/> bezpośrednio obserwujących konto
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Zgłoś to konto</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">FILMY</target>
@@ -6604,31 +6609,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">Nazwa użytkownika skopiowana</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 subskrybujący</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> subskrybujący</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Instancje, które obserwujesz</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Instancje, które Cię obserwują</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Tylko dźwięk</target>
@@ -6678,6 +6673,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target state="translated">Automatycznie (poprzez ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6826,18 +6827,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Domena jest wymagana.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Wprowadzone domeny są nieprawidłowe.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Wprowadzone domeny zawierają duplikaty.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Nieograniczona</target>
@@ -6997,26 +7014,52 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <x id="PH"/> usunięty z obserwujących instancję
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> nie jest prawidowy
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Wysłano prośby o możliwość śledzenia!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Czy na pewno chcesz przestać śledzić 
           <x id="PH"/>?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Przestań śledzić</target>
@@ -7027,8 +7070,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Już nie śledzisz 
           <x id="PH"/>.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="translated">włączona</target>
@@ -7513,9 +7556,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Błąd</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Standardowe raporty</target>
@@ -7560,16 +7603,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Zaktualizuj hasło użytkownika</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Lista obserwujących</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Lista obserwowanych</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Aktualizowano użytkownika <x id="PH"/>.</target>
@@ -7607,16 +7642,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Federacja</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Instancje które obserwujesz</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Instancje które Cię obserwują</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Filmy zostaną usunięte, komentarze zostaną porzucone.</target>
@@ -7647,6 +7674,24 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Ustaw e-mail jako zwerifykowany</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
@@ -7959,13 +8004,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Utworzono kanał wideo 
           <x id="PH"/>.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="translated">Ta nazwa już istnieje na tej instancji.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Zaktualizowano kanał wideo 
@@ -7988,13 +8033,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Usunięto baner.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="translated">Wpisz nazwę kanału (
-          <x id="PH"/>) aby potwierdzić
-        </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Usunięto kanał wideo 
@@ -8160,6 +8199,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target state="translated">Wysłano prośbę o zmianę właściciela.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8262,7 +8307,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Subskrybuj to konto</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">PLAYLISTY</target>
@@ -8309,34 +8354,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="translated">Przejdź do moich subskrypcji</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="translated">Przejdź do moich filmów</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="translated">Przejdź do moich importów</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="translated">Przejdź do moich kanałów</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Nie można uzyskać danych uwierzytelniających klienta OAuth: <x id="PH" equiv-text="error.text"/>. Upewnij się, że PeerTube jest prawidłowo skonfigurowane (katalog config/), szczególnie w sekcji „webserver”.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Musisz połączyć się ponownie.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="translated">Skróty klawiaturowe:</target>
@@ -8349,6 +8394,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8377,38 +8428,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target state="translated">Nieprawidłowa nazwa użytkownika lub hasło.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Twoje konto jest zablokowane</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">jakikolwiek język</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">ukryj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">rozmazanie</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">wyświetl</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Nieznane</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Pomyślnie zresetowano hasło!</target>
@@ -10010,18 +10061,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target>Zbyt wiele prób, spróbuj ponownie za 
           <x id="PH"/> minut.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Zbyt wiele prób, spróbuj ponownie później.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Błąd serwera. Spróbuj ponownie później.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Zasubskrybowano wszystkie kanały użytkownika 
@@ -10621,35 +10672,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275" datatype="html">
         <source>Your video was uploaded to your account and is private.</source>
         <target state="translated">Film został wrzucony na twoje konto i jest prywatny.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Powiązane dane (tagi, opis…) zostaną utracone, czy na pewno chcesz opuścić tą stronę?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Twój film nie został jeszcze wysłany, czy na pewno chcesz opuścić tą stronę?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Wyślij</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">Wrzuć 
           <x id="PH"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Opublikowano film.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119" datatype="html">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target state="translated">Masz niezapisane zmiany! Jeżeli zamkniesz to okno, twoje zmiany zostaną stracone.</target>
@@ -10716,28 +10767,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Ten film nie jest dostępny na tej instancji. Czy chcesz zostać przekierowany(-a) na instancję źródłową: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Przekierowanie</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Ten film zawiera treści wulgarne lub przeznaczone dla dorosłych. Czy na pewno chcesz go obejrzeć?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Zawartość wulgarna lub dla dorosłych</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Następnie</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Anuluj</target>
@@ -10746,63 +10797,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">Automatyczne odtwarzanie jest zatrzymane</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Włącz/wyłącz tryb pełnoekranowy (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Odtwarzaj/spauzuj film (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Wycisz/wyłącz wyciszenie filmu (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Przeskocz do procentowej części filmu: 0 to 0% i 9 to 90% (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Zwiększ głośność (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Zmniejsz głośność (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Przeskocz dalej w filmie (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Przeskocz do tyłu w filmie (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Przyspiesz odtwarzanie (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Zwolnij odtwarzanie (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Przeglądaj film klatka po klatce (kiedy zaznaczony jest odtwarzacz)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="translated">Zaznacz "Lubię to"</target>
index da25265969716b39724517dc9a3e2b1bb425075f..3ee4bd99f659c359c523a04842e3a93065fa59ac 100644 (file)
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="new">My watch history</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Criar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">vídeo</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">subtítulos</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Quota de vídeo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>
         <target state="translated">Federação</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Banir este usuário</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Usuário</target>
         <source>Username or email address</source>
         <target>Nome de usuário ou endereço de e-mail</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Senha</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Clique aqui para redefinir sua senha</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Entrar</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Esqueceu sua senha</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Lamentamos, não podemos recuperar sua senha porque o administrador dessa instância não ativou o envio de e-mail pelo PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1193,26 +1211,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>E-mail</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Endereço de e-mail</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="new">on this instance</target>
@@ -1576,7 +1594,7 @@ The link will expire within 1 hour.</target>
         <target>Criar uma conta</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1644,7 +1662,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1722,8 +1740,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1816,8 +1834,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2579,7 +2597,7 @@ The link will expire within 1 hour.</target>
         <source>Upload on hold</source>
         <target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3296,11 +3314,7 @@ The link will expire within 1 hour.</target>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="translated">Identificador de inscritos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Estado</target>
@@ -3375,11 +3389,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Host</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Redundância permitida 
@@ -3391,9 +3401,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Abrir instância em uma nova aba</target>
@@ -3404,13 +3414,13 @@ The link will expire within 1 hour.</target>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Nenhum host encontrado correspondendo aos filtros atuais.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Sua instância não possui seguidores.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Mostrando 
@@ -3420,16 +3430,8 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Seguir domínios</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3479,11 +3481,11 @@ The link will expire within 1 hour.</target>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Nome de usuário</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3511,9 +3513,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Papel</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">A transcodificação está habilitada. A quota de vídeo só leva em consideração o tamanho do vídeo 
@@ -3535,15 +3537,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3808,6 +3804,12 @@ The link will expire within 1 hour.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -4148,8 +4150,8 @@ The link will expire within 1 hour.</target>
         <target state="new">
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -6138,11 +6140,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">Essa conta não possui canais.</target>
@@ -6187,6 +6186,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6728,12 +6733,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target>Sua mensagem foi enviada.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Você já enviou este formulário recentemente</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="new">Account videos</target>
@@ -6779,13 +6784,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">
           <x id="PH"/> direct account followers
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
@@ -6795,31 +6800,21 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">Nome de usuário copiado</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Áudio-somente</target>
@@ -6869,6 +6864,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target>Automático (via ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -7019,18 +7020,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Ilimitado</target>
@@ -7190,24 +7207,50 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removido dos seguidores da sua instância
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> não é válido
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Solicitação de seguir(s) enviada!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Você realmente deseja parar de seguir <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Parar de seguir</target>
@@ -7216,8 +7259,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>Você não está mais seguindo <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>habilitado</target>
@@ -7702,9 +7745,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Erro</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Registros padrões</target>
@@ -7749,16 +7792,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Atualizar senha do usuário</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7798,16 +7833,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7838,7 +7865,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Definir Email como Confirmado</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
         <target>Você não pode banir root.</target>
@@ -8149,13 +8194,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Canal de vídeo 
           <x id="PH"/> criado.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Este nome já existe nesta instância.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Canal de vídeo 
@@ -8178,13 +8223,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Por favor, digite o nome de exibição do canal (
-          <x id="PH"/>) para confirmar
-        </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Canal de vídeo 
@@ -8346,6 +8385,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target>Solicitação para mudar dono enviada.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8446,7 +8491,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>Inscreva-se na conta</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="new">PLAYLISTS</target>
@@ -8493,35 +8538,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Ir às minhas inscrições</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Ir aos meus vídeos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Ir às minhas importações</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Ir aos meus canais</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>você precisa se reconectar.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Atalhos de teclado:</target>
@@ -8532,6 +8577,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8558,39 +8609,39 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>Nome de usuário ou senha incorretos.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Sua conta foi bloqueada.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Sua senha foi redefinida com sucesso!</target>
@@ -10193,18 +10244,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target>Muitas tentativas, por favor tente novamente depois de 
           <x id="PH"/> minutos.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Muitas tentativas, por favor tente novamente depois.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Erro de servidor. Por favor, tente novamente depois.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Inscrito em todos os canais atuais de 
@@ -10803,34 +10854,34 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target>Seu vídeo foi enviado para sua conta e é privado.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Mas dados associados (tags, descrição…) serão perdidas, tem certeza que deseja sair dessa página?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Seu vídeo ainda não foi atualizado, você tem certeza que deseja sair dessa página?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">Subir 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Vídeo publicado.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>Você tem modificações não salvas! Se sair desta páginas, as modificações serão perdidas.</target>
@@ -10878,27 +10929,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Este vídeo possui conteúdo adulto ou explícito. Você tem certeza que deseja assisti-lo?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Conteúdo adulto ou explícito</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Seguinte</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -10908,62 +10959,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="translated">Auto-leitura está suspensa</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Entrar/Sair da tela cheia (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Tocar/Pausar o vídeo (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Silenciar/Pôr som no vídeo (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Pular para uma percentagem do vídeo: 0 é 0% e 9 é 90% (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Aumentar o volume (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Reduzir o volume (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Procure o vídeo para frente (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Procure o vídeo para trás (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Aumenta a taxa de reprodução (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Diminuir a taxa de reprodução (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Navegue no vídeo quadro por quadro (requer foco do leitor)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Gostar do vídeo</target>
index 2fbff7854fd2f27947f466ea9d661404f3b8671a..6bf8129f3d78ee5893c47676b2fff2f61ec4be50 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source><target state="new">My watch history</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source><target state="new">subtitles</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source> Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Ilimitado 
         <source>Federation</source>
         <target state="translated">Federação</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="translated">seguidores</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Banir este utilizador</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html</context><context context-type="linenumber">16</context></context-group></trans-unit><trans-unit id="7252854992688790751" datatype="html">
         <source> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit><trans-unit id="7215649348148521605" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7215649348148521605" datatype="html">
         <source> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Utilizador</target>
         <source>Username or email address</source>
         <target>Nome de utilizador ou endereço de correio eletrónico</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
+      </trans-unit>
       
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Clique aqui para redefinir a sua senha</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source><target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit><trans-unit id="2101170466365500913" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="2101170466365500913" datatype="html">
         <source> Logging into an account lets you publish content </source><target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Iniciar sessão</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Esqueceu-se da sua palavra-passe</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
         <source> Enter your email address and we will send you a link to reset your password. </source><target state="new"> Enter your email address and we will send you a link to reset your password. </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</source><target state="new">An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</target>
@@ -1132,17 +1150,17 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Endereço de correio eletrónico</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source><target state="new">Reset</target>
         
         <note priority="1" from="description">Password reset button</note>
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       
       <trans-unit id="4319634264526091601" datatype="html">
@@ -1495,7 +1513,7 @@ The link will expire within 1 hour.</target>
         <source>Create an account</source>
         <target>Criar uma conta</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source><target state="new">My videos</target>
@@ -1541,7 +1559,7 @@ The link will expire within 1 hour.</target>
         <target state="new">VIDEOS</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="new">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1609,7 +1627,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/menu/notification.component.html</context><context context-type="linenumber">49</context></context-group></trans-unit><trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source><target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source><target state="new">That's an error.</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context>
@@ -1675,7 +1693,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context><context context-type="linenumber">42</context></context-group></trans-unit><trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source><target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2365,7 +2383,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3029,11 +3047,7 @@ The link will expire within 1 hour.</target>
         <target>ID</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group></trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Estado</target>
@@ -3115,11 +3129,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3130,7 +3140,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source><target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Abre instância numa nova tabulação</target>
@@ -3142,12 +3152,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="translated">Não encontrou algum host que corresponda aos filtros actuais.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">A sua instância não está a seguir alguém.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">
@@ -3157,14 +3167,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Siga domínios</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit><trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source><target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3214,7 +3217,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source><target state="new">e.g. jane_doe</target>
         
         <note priority="1" from="description">Username choice placeholder in the registration form</note>
@@ -3242,7 +3245,7 @@ The link will expire within 1 hour.</target>
         <target>Papel</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source> Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Transcodificação está activada. A quota de vídeos tem em conta apenas o tamanho de vídeo 
@@ -3262,15 +3265,9 @@ The link will expire within 1 hour.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="new">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3495,7 +3492,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="4691552465058437520" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit><trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source><target state="new">Commented video</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">82</context></context-group></trans-unit><trans-unit id="7266085473379376028" datatype="html">
@@ -3813,7 +3816,7 @@ The link will expire within 1 hour.</target>
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Parece que não está em um servidor HTTPS. O seu servidor necessita de ter TLS activado para conseguir seguir outros servidores.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Silenciar domínios</target>
@@ -5618,11 +5621,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5660,7 +5660,13 @@ channel with the same name (<x id="PH_2"/>)!</source><target state="new">Do you
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6250,12 +6256,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source><target state="new">Account videos</target>
         
@@ -6290,10 +6296,10 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source><target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source><target state="new">VIDEOS</target>
@@ -6305,24 +6311,16 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source><target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source><target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6370,7 +6368,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="931255636742351800" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit><trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source><target state="new">No limit</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="5250062810079582285" datatype="html">
@@ -6489,17 +6493,33 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group></trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
+      </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Ilimitado</target>
@@ -6630,7 +6650,27 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="2740793005745065895">
         <source>
           <x id="PH"/> is not valid
@@ -6639,19 +6679,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> não é válido
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Solicitação de seguir(s) enviada!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Você realmente deseja parar de seguir 
           <x id="PH"/>?
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Parar de seguir</target>
@@ -6663,7 +6709,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/>.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7121,7 +7167,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7159,13 +7205,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Update user password</source>
         <target state="new">Update user password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit><trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source><target state="new">Following list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group></trans-unit><trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source><target state="new">Followers list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit>
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7196,13 +7236,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/users.routes.ts</context><context context-type="linenumber">45</context></context-group></trans-unit><trans-unit id="8564701209009684429" datatype="html">
         <source>Federation</source><target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source><target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group></trans-unit><trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source><target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7230,7 +7264,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
         <target>Você não pode banir root.</target>
@@ -7535,12 +7587,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> criado.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Canal de vídeo 
@@ -7557,13 +7609,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Canal de vídeo 
@@ -7706,7 +7752,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target>Solicitação para mudar dono enviada.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
         <target>Meus canais</target>
@@ -7801,7 +7853,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7848,34 +7900,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source><target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       
       
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>você precisa se reconectar.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -7886,6 +7938,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -7909,38 +7967,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target>Nome de usuário ou senha incorretos.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Sua senha foi redefinida com sucesso!</target>
@@ -9484,17 +9542,17 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/> minutos.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Muitas tentativas, por favor tente novamente depois.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Erro de servidor. Por favor, tente novamente depois.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -9992,20 +10050,20 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target>Seu vídeo foi enviado para sua conta e é privado.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Mas dados associados (tags, descrição…) serão perdidas, tem certeza que deseja sair dessa página?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Seu vídeo ainda não foi atualizado, você tem certeza que deseja sair dessa página?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source><target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload 
           <x id="PH"/>
@@ -10014,13 +10072,13 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Vídeo publicado.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119">
@@ -10070,26 +10128,26 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source><target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source><target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Este vídeo possui conteúdo adulto ou explícito. Você tem certeza que deseja assisti-lo?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Conteúdo adulto ou explícito</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source><target state="new">Cancel</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">646</context></context-group></trans-unit>
@@ -10097,62 +10155,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index d72d8420d97accbdd42f105d3603c1a840d15a28..af1ceec569d43ce178ec3ace43b60072bf7f9a93 100644 (file)
       <trans-unit id="187187500641108332" datatype="html">
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Моя история просмотров</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Создать</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">видео</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">Следующая ссылка содержит личный токен и никому не может быть передана.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">Ваша квота для этого видео превышена (размер видео: <x id="PH" equiv-text="videoSizeBytes"/>, использовано: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, квота: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">Ваша дневная квота для этого видео превышена (размер видео: <x id="PH" equiv-text="videoSizeBytes"/>, использовано: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, квота: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">субтитры</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Квота на видео</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Неограниченно <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> в день)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target state="translated">Федерация</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
       <trans-unit id="2159130950882492111">
         <source>Cancel</source>
         <target>Отменить</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Заблокировать этого пользователя</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Этот экземпляр разрешает регистрацию. Однако будьте осторожны, проверьте <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Условия пользования<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> перед созданием учетной записи. Вы также можете найти другой экземпляр, который точно соответствует вашим потребностям, на: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">В настоящее время этот экземпляр не позволяет регистрировать пользователей, проверьте <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Условия пользования<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> для получения дополнительных сведений, или найдите экземпляр, который дает вам возможность зарегистрировать учетную запись и загружать туда свои видео. Найдите свой среди множества экземпляров на: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Пользователь</target>
         <source>Username or email address</source>
         <target>Имя пользователя или электронный адрес</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Пароль</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Нажмите здесь что бы сбросить пароль</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Я забыл свой пароль</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Авторизация учетной записи позволяет публиковать контент</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Авторизация</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Или войдите с помощью</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Забыли пароль</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">К сожалению, вы не можете восстановить свой пароль, так как администратор вашего экземпляра не настроил почтовую систему PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Введите свой email и мы пришлём вам ссылку для сброса пароля.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1089,26 +1107,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Email</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Email адрес</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Сброс</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">на этом экземпляре</target>
@@ -1438,9 +1456,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>Создать учетную запись</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">Мои видео</target>
@@ -1507,10 +1525,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">ВИДЕО</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">Параллельный импорт заданий</target>
@@ -1589,8 +1607,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">Я чайник</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Это ошибка.</target>
@@ -1683,8 +1701,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">Видео слишком большое для сервера. Пожалуйста свяжитесь со вашим администратором для увеличение лимита.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">ГЛОБАЛЬНЫЙ ПОИСК</target>
@@ -2424,8 +2442,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="translated">Загрузка приостановлена</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Извините, загрузка файлов недоступна для вашей учётной записи. Если вы хотите добавлять видео, свяжитесь с администратором.</target>
@@ -3117,11 +3135,7 @@ The link will expire within 1 hour.</source>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="translated">Подписчик</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Состояние</target>
@@ -3187,11 +3201,7 @@ The link will expire within 1 hour.</source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action }}"/> </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Хост</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Избыточность разрешена <x id="START_TAG_P-SORTICON"/> <x id="CLOSE_TAG_P-SORTICON"/></target>
@@ -3200,9 +3210,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Отписаться</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Открыть экземпляр в новой вкладке</target>
@@ -3213,28 +3223,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Не найдено ни одного хоста, соответствующего текущим фильтрам.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Ваш экземпляр ни за кем не подписан.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Показано <x id="INTERPOLATION"/> по <x id="INTERPOLATION_1"/> из <x id="INTERPOLATION_2"/> хостов</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Следить за доменами</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Следить за экземплярами</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">Действие</target>
@@ -3284,11 +3286,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Имя пользователя</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">прим. ivan_ivanov</target>
@@ -3316,9 +3318,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Роль</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Транскодирование включено. Квота видео учитывает только <x id="START_TAG_STRONG"/>оригинальный<x id="CLOSE_TAG_STRONG"/> размер видео. <x id="LINE_BREAK"/>Максимум, этот пользователь мог загрузить ~ <x id="INTERPOLATION"/>. </target>
@@ -3335,15 +3337,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Плагин авторизации</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Нет (локальная аутентификация)</target>
@@ -3594,6 +3590,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3895,8 +3897,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Похоже, вы не на сервере HTTPS. На вашем веб-сервере должен быть активирован TLS, чтобы следить за серверами.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Отключить домены</target>
@@ -5839,11 +5841,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">КАНАЛЫ</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">Эта учетная запись не имеет каналов.</target>
@@ -5882,6 +5881,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Вы действительно хотите удалить <x id="PH" equiv-text="videoChannel.displayName"/>? Будет удалено <x id="PH_1" equiv-text="videoChannel.videosCount"/> видео загруженное на этот канал, и вы не сможете создать другой канал с таким же именем (<x id="PH_2" equiv-text="videoChannel.name"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6397,13 +6402,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="6979021199788941693">
         <source>Your message has been sent.</source>
         <target>Ваше сообщение было отправлено.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Вы уже отправили эту форму совсем недавно</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Видео аккаунта</target>
@@ -6446,13 +6451,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="4856575356061361269" datatype="html">
         <source><x id="PH"/> direct account followers </source>
         <target state="translated"><x id="PH"/> прямые подписчики аккаунта </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Пожаловаться на этот аккаунт</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">ВИДЕО</target>
@@ -6462,31 +6467,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">Имя пользователя скопировано</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 подписчик</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> подписчиков</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Экземпляры, за которыми вы следите</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Следующие за вами экземпляры</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Только для аудио</target>
@@ -6536,6 +6531,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target>Авто (используя ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6684,18 +6685,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Требуется домен.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Введенные домены недействительны.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Введенные домены содержат дубликаты.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Неограниченно</target>
@@ -6853,24 +6870,50 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <x id="PH"/> удалено из последователей экземпляра
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> недействителен
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Запрос (ы) на подписку отправлены!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Вы действительно хотите отписаться от <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Отписаться</target>
@@ -6879,8 +6922,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>Вы больше не подписаны на <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>включено</target>
@@ -7352,9 +7395,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Ошибка</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Стандартные журналы</target>
@@ -7395,16 +7438,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Изменить пароль пользователя</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Следующий список</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Список подписчиков</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Пользователь <x id="PH"/> обновлён.</target>
@@ -7440,16 +7475,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Федерация</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Экземпляры, за которыми вы следите</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Экземпляры подписанные на вас</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Видео будет удалено, комментарии будут заморожены.</target>
@@ -7480,6 +7507,24 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Пометить электронную почту как подтверждённую</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
@@ -7784,13 +7829,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1137937154872046253">
         <source>Video channel <x id="PH"/> created.</source>
         <target>Видеоканал <x id="PH"/> был создан.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Данное название уже занято на этом сервере.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Видеоканал <x id="PH"/> обновлён.</target>
@@ -7811,11 +7856,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Баннер удален.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Пожалуйста, напишите название канала ( <x id="PH"/>) для подтверждения</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Видеоканал <x id="PH"/> был удалён.</target>
@@ -7967,6 +8008,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target>Заявка на смена владельца отправлена.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8065,7 +8112,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Подписаться на аккаунт</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">ПЛЕЙЛИСТЫ</target>
@@ -8112,34 +8159,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Перейти на мои подписки</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Перейти на мои видео</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Перейти на мои импортированные видео</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Перейти на мои каналы</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Не удается получить учетные данные клиента OAuth: <x id="PH" equiv-text="error.text"/>. Убедитесь, что вы правильно настроили PeerTube (config / directory), в частности раздел «веб-сервер».</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Вам необходимо переподключиться.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Комбинации клавиш:</target>
@@ -8152,6 +8199,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8180,38 +8233,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>Неверное имя пользователя или пароль.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Ваш аккаунт заблокирован.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">любой язык</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">скрыть</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">размытие</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">отображение</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Неизвестно</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Ваш пароль был успешно сброшен!</target>
@@ -9783,18 +9836,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>Слишком много попыток, пожалуйста, попробуйте снова через <x id="PH"/> минут.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Слишком много попыток, пожалуйста, попробуйте ещё раз позже.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Ошибка сервера. Пожалуйста, повторите попытку позже.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Подписан на все текущие каналы <x id="PH"/>. Вы будете уведомлены обо всех их новых видео.</target>
@@ -10385,33 +10438,33 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275">
         <source>Your video was uploaded to your account and is private.</source>
         <target>Ваше видео было загружено на ваш аккаунт и является приватным.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Но связанные данные (теги, описание...) будут потеряны, вы уверены, что хотите покинуть эту страницу?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Ваше видео еще не загружено, вы уверены, что хотите покинуть эту страницу?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Загрузить</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">Загрузить <x id="PH"/> </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Видео опубликовано.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>У вас есть несохраненные изменения! Если вы уйдете, ваши изменения будут потеряны.</target>
@@ -10458,28 +10511,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Это видео недоступно в этом экземпляре. Вы хотите, чтобы вас перенаправили на исходный экземпляр: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Перенаправление</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Это видео содержит зрелый или откровенный контент. Вы уверены, что хотите посмотреть его?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Зрелый или откровенный контент</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Следующий</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Отмена</target>
@@ -10488,63 +10541,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">Автовоспроизведение приостановлено</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Вход / выход в полноэкранный режим (требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Воспроизвести / приостановить видео (требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Отключить / включить видео (требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Переход к проценту от видео: 0 - 0%, 9 - 90% (требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Увеличьте громкость (требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Уменьшить громкость (требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Искать видео вперед(требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Искать видео в обратном направлении (требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Увеличить скорость воспроизведения(требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Уменьшить скорость воспроизведения (требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Navigate in the video frame by frame (требуется фокус проигрывателя)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Мне понравилось</target>
index 87a114685e0951fb0b84d44fbee13c62dddc2934..4cd53b149925e96a08afa2e201ea32bde21da870 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source><target state="new">My watch history</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source><target state="new">subtitles</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source> Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="new">
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="new">Ban this user</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html</context><context context-type="linenumber">16</context></context-group></trans-unit><trans-unit id="7252854992688790751" datatype="html">
         <source> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit><trans-unit id="7215649348148521605" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7215649348148521605" datatype="html">
         <source> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729" datatype="html">
         <source>User</source>
         <target state="new">User</target>
         <source>Username or email address</source>
         <target state="new">Username or email address</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
+      </trans-unit>
       
       <trans-unit id="1431416938026210429" datatype="html">
         <source>Password</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source><target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit><trans-unit id="2101170466365500913" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="2101170466365500913" datatype="html">
         <source> Logging into an account lets you publish content </source><target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966" datatype="html">
         <source>Login</source>
         <target state="new">Login</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367" datatype="html">
         <source>Forgot your password</source>
         <target state="new">Forgot your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
         <source> Enter your email address and we will send you a link to reset your password. </source><target state="new"> Enter your email address and we will send you a link to reset your password. </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</source><target state="new">An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</target>
@@ -1157,17 +1175,17 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610" datatype="html">
         <source>Email address</source>
         <target state="new">Email address</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source><target state="new">Reset</target>
         
         <note priority="1" from="description">Password reset button</note>
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       
       <trans-unit id="4319634264526091601" datatype="html">
@@ -1537,7 +1555,7 @@ The link will expire within 1 hour.</target>
         <source>Create an account</source>
         <target state="new">Create an account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source><target state="new">My videos</target>
@@ -1583,7 +1601,7 @@ The link will expire within 1 hour.</target>
         <target state="new">VIDEOS</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="new">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1651,7 +1669,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/menu/notification.component.html</context><context context-type="linenumber">49</context></context-group></trans-unit><trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source><target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source><target state="new">That's an error.</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context>
@@ -1717,7 +1735,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context><context context-type="linenumber">42</context></context-group></trans-unit><trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source><target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2425,7 +2443,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3097,11 +3115,7 @@ The link will expire within 1 hour.</target>
         <target state="new">ID</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group></trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3183,11 +3197,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3198,7 +3208,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source><target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3210,12 +3220,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3225,14 +3235,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit><trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source><target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3282,7 +3285,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source><target state="new">e.g. jane_doe</target>
         
         <note priority="1" from="description">Username choice placeholder in the registration form</note>
@@ -3314,7 +3317,7 @@ The link will expire within 1 hour.</target>
         <target state="new">Role</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source> Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3337,15 +3340,9 @@ The link will expire within 1 hour.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="new">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3572,7 +3569,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="4691552465058437520" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit><trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source><target state="new">Commented video</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">82</context></context-group></trans-unit><trans-unit id="7266085473379376028" datatype="html">
@@ -3896,7 +3899,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5711,11 +5714,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5753,7 +5753,13 @@ channel with the same name (<x id="PH_2"/>)!</source><target state="new">Do you
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6365,12 +6371,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source><target state="new">Account videos</target>
         
@@ -6405,10 +6411,10 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source><target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source><target state="new">VIDEOS</target>
@@ -6420,24 +6426,16 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source><target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source><target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6485,7 +6483,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="931255636742351800" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit><trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source><target state="new">No limit</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="5250062810079582285" datatype="html">
@@ -6604,17 +6608,33 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group></trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
+      </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="new">Unlimited</target>
@@ -6745,7 +6765,27 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source>
           <x id="PH"/> is not valid
@@ -6754,19 +6794,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> is not valid
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
@@ -6778,7 +6824,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> anymore.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7236,7 +7282,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7274,13 +7320,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Update user password</source>
         <target state="new">Update user password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit><trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source><target state="new">Following list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group></trans-unit><trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source><target state="new">Followers list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit>
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7311,13 +7351,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/users.routes.ts</context><context context-type="linenumber">45</context></context-group></trans-unit><trans-unit id="8564701209009684429" datatype="html">
         <source>Federation</source><target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source><target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group></trans-unit><trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source><target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7345,7 +7379,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -7650,12 +7702,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> created.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="new">Video channel 
@@ -7672,13 +7724,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="new">Video channel 
@@ -7821,7 +7867,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
         <target state="new">My channels</target>
@@ -7916,7 +7968,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7963,34 +8015,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source><target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       
       
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -8001,6 +8053,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8024,38 +8082,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="new">Your password has been successfully reset!</target>
@@ -9598,17 +9656,17 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/> minutes.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10111,20 +10169,20 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="new">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source><target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload 
           <x id="PH"/>
@@ -10133,13 +10191,13 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="new">Video published.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10203,26 +10261,26 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source><target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source><target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source><target state="new">Cancel</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">646</context></context-group></trans-unit>
@@ -10230,62 +10288,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index 582be2864b91b500b8849ac5edc1049a126bd523..20cab08083a7fca5e7cc2b4de5c3b3fba94bb103 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="new">My watch history</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="new"> The following link contains a private token and should not be shared with anyone. </target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="new">subtitles</target>
       <trans-unit id="2602586221576511475" datatype="html">
         <source>Video quota</source>
         <target state="new">Video quota</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="new">
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="new">Ban this user</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729" datatype="html">
         <source>User</source>
         <target state="new">User</target>
         <source>Username or email address</source>
         <target>Uporabniško ime ali e-poštni naslov</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Geslo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Prijava</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Ste pozabili geslo?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1186,26 +1204,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>E-poštni naslov</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>E-poštni naslov</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="new">on this instance</target>
@@ -1579,7 +1597,7 @@ The link will expire within 1 hour.</target>
         <target>Ustvari račun</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1636,7 +1654,7 @@ The link will expire within 1 hour.</target>
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1715,7 +1733,7 @@ The link will expire within 1 hour.</target>
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1808,8 +1826,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2555,7 +2573,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3262,11 +3280,7 @@ The link will expire within 1 hour.</target>
         <target state="new">ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3341,11 +3355,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3358,7 +3368,7 @@ The link will expire within 1 hour.</target>
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3369,13 +3379,13 @@ The link will expire within 1 hour.</target>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3385,16 +3395,8 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3443,11 +3445,11 @@ The link will expire within 1 hour.</target>
       <trans-unit id="5248717555542428023" datatype="html">
         <source>Username</source>
         <target state="new">Username</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3477,9 +3479,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target state="new">Role</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3504,15 +3506,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3766,6 +3762,12 @@ The link will expire within 1 hour.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -4104,8 +4106,8 @@ The link will expire within 1 hour.</target>
         <target state="new">
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -6102,11 +6104,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="new">This account does not have channels.</target>
@@ -6151,6 +6150,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6749,12 +6754,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="new">Account videos</target>
@@ -6800,13 +6805,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">
           <x id="PH"/> direct account followers
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
@@ -6816,29 +6821,21 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="new">Username copied</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6888,6 +6885,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -7030,18 +7033,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="new">Unlimited</target>
@@ -7201,26 +7220,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="new">
           <x id="PH"/> is not valid
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
@@ -7231,8 +7276,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">You are not following 
           <x id="PH"/> anymore.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7723,9 +7768,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="new">Error</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7770,16 +7815,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7819,16 +7856,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7859,7 +7888,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -8172,13 +8219,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Video channel 
           <x id="PH"/> created.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="new">Video channel 
@@ -8201,13 +8248,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="new">Video channel 
@@ -8370,6 +8411,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8472,7 +8519,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="new">PLAYLISTS</target>
@@ -8519,35 +8566,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -8558,6 +8605,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8580,39 +8633,39 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="new">Your password has been successfully reset!</target>
@@ -10209,18 +10262,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target state="new">Too many attempts, please try again after 
           <x id="PH"/> minutes.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10818,35 +10871,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="new">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="new">Upload 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="new">Video published.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10916,27 +10969,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -10946,62 +10999,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index f36bd5c46e1d1156ff10de615996af98888188d8..1c367146806e7ddb1f43ebdb79ce850855092cb6 100644 (file)
         <target state="translated">
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Min visningshistorik</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Skapa</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">video</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">Följande länk innehåller en personlig nyckel och bör ej delas med någon annan.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">Din videokvot kommer överskridas av den här videon (videostorlek: <x id="PH" equiv-text="videoSizeBytes"/>, använt: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, kvot: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">Din dagliga videokvot kommer överskridas av den här videon (videostorlek: <x id="PH" equiv-text="videoSizeBytes"/>, använt: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, kvot:<x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">undertexter</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Videokvot</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>Obegränsat <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per dag)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target>Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106">
         <source>followers</source>
       <trans-unit id="2159130950882492111">
         <source>Cancel</source>
         <target>Avbryt</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Blockera den här användaren</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Den här instansen tillåter kontoregistrering. Se till att läsa <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>villkoren<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>villkoren<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> innan du skapar ett konto. Du kan också söka efter en annan instans som passar dina behov bättre på <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Den här instansen tillåter inte kontoregistrering för närvarande, men du kan läsa <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>villkoren<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> för mer information eller hitta en annan instans som ger dig möjligheten att skaffa ett konto och ladda upp dina videor där. Hitta din instans av dem alla på <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Användare</target>
         <source>Username or email address</source>
         <target>Användarnamn eller e-postadress</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Lösenord</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Klicka här för att återställa ditt lösenord</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Jag har glömt mitt lösenord</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Du måste logga in för att kunna publicera material</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Logga in</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Eller logga in med</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Glömt ditt lösenord</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target>
       Du kan inte återställa ditt lösenord eftersom din instans administratör inte har  konfigurerat PeerTubes e-postsystem.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Ange din e-postadress så skickar vi dig en länk för att återställa till lösenord.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1101,26 +1119,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>E-post</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>E-postadress</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Återställ</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">på den här instansen</target>
@@ -1455,9 +1473,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>Skapa ett konto</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">Mina videor</target>
@@ -1524,10 +1542,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDEOR</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">Samtidiga importjobb</target>
@@ -1606,8 +1624,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">Jag är en tekanna</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Detta är ett fel.</target>
@@ -1700,8 +1718,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">Filen är för stor för servern. Kontakta din administratör om du vill höja gränsen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">GLOBAL SÖKNING</target>
@@ -2445,8 +2463,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="translated">Uppladdning pausad</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Uppladdning är inte aktiverat från ditt konto. Om du vill lägga upp videor, måste en administratör låsa upp din videokvot.</target>
@@ -3133,11 +3151,7 @@ The link will expire within 1 hour.</source>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target>Hantera följare</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Status</target>
@@ -3205,11 +3219,7 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>Värd</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Redundans tillåten <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3218,9 +3228,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Sluta följa</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Öppna instansen i en ny flik</target>
@@ -3231,28 +3241,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Inga värdar matchar de nuvarande filtren.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Din instans följer inte någon annan.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Visar värd <x id="INTERPOLATION"/> till <x id="INTERPOLATION_1"/> av <x id="INTERPOLATION_2"/></target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Följ domäner</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Följ instanser</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">Åtgärd</target>
@@ -3302,11 +3304,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Användarnamn</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">t.ex. jane_doe</target>
@@ -3334,9 +3336,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Roll</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Omkodning har aktiverats. Videokvoten omfattar endast <x id="START_TAG_STRONG"/>originalfilens<x id="CLOSE_TAG_STRONG"/> storlek. <x id="LINE_BREAK"/> Den här användaren kan ladda upp ungefär <x id="INTERPOLATION"/>. </target>
@@ -3353,15 +3355,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Tillägg för autentisering</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Ingen (lokal autentisering)</target>
@@ -3612,6 +3608,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3913,8 +3915,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Det verkar som att din server inte använder HTTPS. Webbserver måste ha TLS aktiverat för att följa andra servrar.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Ignorera instanser</target>
@@ -5868,11 +5870,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">KANALER</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
         <target>Det här kontot har inga kanaler.</target>
@@ -5911,6 +5910,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Vill du verkligen radera <x id="PH" equiv-text="videoChannel.displayName"/>? Det kommer att radera <x id="PH_1" equiv-text="videoChannel.videosCount"/> videor uppladdade till kanalen, och du kan inte skapa en kanal med samma namn (<x id="PH_2" equiv-text="videoChannel.name"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6430,13 +6435,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="6979021199788941693">
         <source>Your message has been sent.</source>
         <target>Ditt meddelande har skickats.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>Du har redan skickat detta formulär nyligen</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Kontots videor</target>
@@ -6481,13 +6486,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Kontot har 
           <x id="PH"/> direktföljare
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Anmäl det här kontot</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDEOR</target>
@@ -6497,31 +6502,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533">
         <source>Username copied</source>
         <target>Användarnamn kopierat</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 prenumerant</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> prenumeranter</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Instanser du följer</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Instanser som följer dig</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Endast ljud</target>
@@ -6571,6 +6566,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target>Automatiskt (via ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6719,18 +6720,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Domän måste uppges.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Angivna domäner är ogiltiga.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Domänerna innehåller dubbletter.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>Obegränsat</target>
@@ -6890,24 +6907,50 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <x id="PH"/> har tagits bort från listan över instansföljare
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> är inte giltig
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>Följningsförfrågan / förfrågningar skickad!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>Vill du verkligen sluta följa <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>Sluta följa</target>
@@ -6916,8 +6959,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>Du följer inte <x id="PH"/> längre.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>aktiverad</target>
@@ -7389,9 +7432,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>Fel</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Standardloggar</target>
@@ -7432,16 +7475,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Uppdatera användarens lösenord</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Lista över följda instanser</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Lista över följare</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Användaren <x id="PH"/> uppdaterad.</target>
@@ -7477,16 +7512,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Instanser du följer</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Instanser som följer dig</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Videorna kommer raderas och kommentarerna arkiverade.</target>
@@ -7517,6 +7544,24 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Markera e-post som verifierad</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
@@ -7821,13 +7866,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1137937154872046253">
         <source>Video channel <x id="PH"/> created.</source>
         <target>Kanalen <x id="PH"/> har skapats.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>Namnet finns redan på den här instansen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>Kanalen <x id="PH"/> har uppdaterats.</target>
@@ -7848,11 +7893,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Baneret har raderats.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>Uppge kanalens visningsnamn (<x id="PH"/>) för att bekräfta</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>Kanalen <x id="PH"/> har raderats.</target>
@@ -8004,6 +8045,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target>Förfrågan om byte av ägarskap har skickats.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8102,7 +8149,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>Prenumerera på kontot</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">SPELLISTOR</target>
@@ -8149,34 +8196,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>Gå till mina prenumerationer</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>Gå till mina videor</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>Gå till mina importeringar</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>Gå till mina kanaler</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Kan inte hämta OAuth Client-uppgifter: <x id="PH" equiv-text="error.text"/>. Försäkra dig om att du har konfigurerat PeerTube korrekt (i config-katalogen), speciellt ”webserver”-sektionen.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>Du måste återansluta.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Kortkommandon:</target>
@@ -8189,6 +8236,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8217,38 +8270,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>Felaktigt användarnamn eller lösenord.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Ditt konto har blockerats.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">vilket språk som helst</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">dölj</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">gör suddig</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">visa</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Okänd</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>Ditt lösenord har återställts!</target>
@@ -9834,18 +9887,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>För många försök, vänligen försök igen om <x id="PH"/> minuter.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>För många försök, vänligen försök igen senare.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Serverfel, försök gärna igen om en stund.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Prenumererar på samtliga kanaler tillhörande <x id="PH"/>. Du kommer underrättas om alla nya videor.</target>
@@ -10438,35 +10491,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275">
         <source>Your video was uploaded to your account and is private.</source>
         <target>Din video har laddats upp till ditt konto och är privat.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Men associerad data (taggar, beskrivning …) kommer försvinna, är du säker på att du vill lämna sidan?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Din video har inte laddats upp än, vill du lämna sidan?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Ladda upp</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload <x id="PH"/> </source>
         <target>Ladda upp 
           <x id="PH"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Videon har publicerats.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>Du har gjort ändringar som inte sparats! Om du lämnar nu kommer de förkastas.</target>
@@ -10513,28 +10566,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Den här videon finns inte på din instans. Vill du bli hänvisad till ursprungsinstansen &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Omdirigering</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Den här videon innehåller oförbehållsamt innehåll eller innehåll skapat för vuxna. Är du säker på att du vill se den?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Oförbehållsamt innehåll eller innehåll skapat för vuxna</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Kommer härnäst</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Avbryt</target>
@@ -10543,63 +10596,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">Automatisk uppspelning är upphävd</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Slå av eller på helskärmsläget (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Spela eller pausa videon (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Slå av eller på ljudet (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Hoppa till en position i videon: 0 motsvarar 0 % av den totala speltiden och 9 motsvarar 90 % (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Öka volymen (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Minska volymen (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Spola videon framåt (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Spola videon bakåt (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Öka uppspelningshastigheten (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Minska uppspelningshastigheten (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Föregående eller nästkommande bildruta (spelaren måste vara markerad)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Gilla videon</target>
index 4dd2b73cdec86d2dc7ed8cf82adf17f8d882e27d..e7757cc6b72a73978229701e3d9363b94dede88f 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit><trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source><target state="new">My watch history</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-history/my-history.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="new">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit><trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source><target state="new">subtitles</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source> Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="new">
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>இந்த பயணரை ரத்து செய்</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html</context><context context-type="linenumber">16</context></context-group></trans-unit><trans-unit id="7252854992688790751" datatype="html">
         <source> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit><trans-unit id="7215649348148521605" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7215649348148521605" datatype="html">
         <source> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source><target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>பயணர்</target>
         <source>Username or email address</source>
         <target state="new">Username or email address</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
+      </trans-unit>
       
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="new">Click here to reset your password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source><target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit><trans-unit id="2101170466365500913" datatype="html">
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit><trans-unit id="2101170466365500913" datatype="html">
         <source> Logging into an account lets you publish content </source><target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>உள்நுழை</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="new">Or sign in with</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>கடவுச்சொல் மறந்துவிட்டது</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
       We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="3188014010833256853" datatype="html">
         <source> Enter your email address and we will send you a link to reset your password. </source><target state="new"> Enter your email address and we will send you a link to reset your password. </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit><trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</source><target state="new">An email with the reset password instructions will be sent to <x id="PH"/>.
 The link will expire within 1 hour.</target>
@@ -1157,17 +1175,17 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>மின்னஞ்சல்</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit><trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source><target state="new">Reset</target>
         
         <note priority="1" from="description">Password reset button</note>
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       
       <trans-unit id="4319634264526091601" datatype="html">
@@ -1537,7 +1555,7 @@ The link will expire within 1 hour.</target>
         <source>Create an account</source>
         <target>கணக்கை உருவாக்கு</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source><target state="new">My videos</target>
@@ -1583,7 +1601,7 @@ The link will expire within 1 hour.</target>
         <target state="new">VIDEOS</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="new">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1651,7 +1669,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/menu/notification.component.html</context><context context-type="linenumber">49</context></context-group></trans-unit><trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source><target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit><trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source><target state="new">That's an error.</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context>
@@ -1717,7 +1735,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.html</context><context context-type="linenumber">42</context></context-group></trans-unit><trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source><target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2425,7 +2443,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3097,11 +3115,7 @@ The link will expire within 1 hour.</target>
         <target>ID</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group></trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3183,11 +3197,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3198,7 +3208,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source><target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3210,12 +3220,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3225,14 +3235,7 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit><trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source><target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3282,7 +3285,7 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit><trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source><target state="new">e.g. jane_doe</target>
         
         <note priority="1" from="description">Username choice placeholder in the registration form</note>
@@ -3314,7 +3317,7 @@ The link will expire within 1 hour.</target>
         <target state="new">Role</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source> Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3337,15 +3340,9 @@ The link will expire within 1 hour.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="new">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3572,7 +3569,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="4691552465058437520" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit><trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source><target state="new">Commented video</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">82</context></context-group></trans-unit><trans-unit id="7266085473379376028" datatype="html">
@@ -3896,7 +3899,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5711,11 +5714,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5753,7 +5753,13 @@ channel with the same name (<x id="PH_2"/>)!</source><target state="new">Do you
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6365,12 +6371,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source><target state="new">Account videos</target>
         
@@ -6405,10 +6411,10 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit><trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source><target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source><target state="new">VIDEOS</target>
@@ -6420,24 +6426,16 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Username copied</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit><trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source><target state="new">1 subscriber</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit><trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source><target state="new"><x id="PH"/> subscribers</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6485,7 +6483,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="931255636742351800" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit><trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source><target state="new">No limit</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit><trans-unit id="5250062810079582285" datatype="html">
@@ -6604,17 +6608,33 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group></trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
+      </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="new">Unlimited</target>
@@ -6745,7 +6765,27 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source>
           <x id="PH"/> is not valid
@@ -6754,19 +6794,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> is not valid
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
@@ -6778,7 +6824,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> anymore.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7236,7 +7282,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7274,13 +7320,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Update user password</source>
         <target state="new">Update user password</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit><trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source><target state="new">Following list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group></trans-unit><trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source><target state="new">Followers list</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group></trans-unit>
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7311,13 +7351,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/users.routes.ts</context><context context-type="linenumber">45</context></context-group></trans-unit><trans-unit id="8564701209009684429" datatype="html">
         <source>Federation</source><target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit><trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source><target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group></trans-unit><trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source><target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7345,7 +7379,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -7650,12 +7702,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> created.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="new">Video channel 
@@ -7672,13 +7724,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="new">Video channel 
@@ -7821,7 +7867,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group></trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
         <target state="new">My channels</target>
@@ -7916,7 +7968,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -7963,34 +8015,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit><trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source><target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       
       
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -8001,6 +8053,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8024,38 +8082,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="new">Your password has been successfully reset!</target>
@@ -9598,17 +9656,17 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/> minutes.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10111,20 +10169,20 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="new">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit><trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source><target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload 
           <x id="PH"/>
@@ -10133,13 +10191,13 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="new">Video published.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10203,26 +10261,26 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source><target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit><trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source><target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit><trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source><target state="new">Cancel</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">646</context></context-group></trans-unit>
@@ -10230,62 +10288,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index 4cfdc4f61a9feffee4ee34db1540897d4a3e8860..7ffd05f1a0ec3bd27b7bbb87fcd398094bb1b206 100644 (file)
         <target state="new">
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="new">My watch history</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>สร้าง</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">วิดีโอ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="new"> The following link contains a private token and should not be shared with anyone. </target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">คำบรรยายใต้ภาพ</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>ปริมาณวิดีโอที่สามารถอัปโหลดได้</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>
         <target state="translated">รวมเว็บไซต์อื่น</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
       <trans-unit id="2159130950882492111">
         <source>Cancel</source>
         <target>ยกเลิก</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>แบนผู้ใช้นี้</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">เซิร์ฟเวอร์นี้เปิดให้ลงทะเบียนผู้ใช้ใหม่ อย่างไรก็ตาม โปรดตรวจสอบ<x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>เงื่อนไข<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>เงื่อนไข<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>อย่างระมัดระวังก่อนการสร้างบัญชี คุณสามารถหาเซิร์ฟเวอร์อื่นที่ตรงกับความต้องการของคุณโดยเฉพาะได้ที่: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">ณ ตอนนี้ เซิร์ฟเวอร์นี้ไม่เปิดให้ลงทะเบียนผู้ใช้ใหม่ คุณสามารถตรวจสอบ<x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>เงื่อนไข<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>เพื่อดูข้อมูลเพิ่มเติมหรือหาเซิร์ฟเวอร์อื่นที่ให้คุณสร้างบัญชีและอัปโหลดวิดีโอได้ หาเว็บไซต์ที่เหมาะกับคุณได้ที่: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>ผู้ใช้</target>
         <source>Username or email address</source>
         <target>ชื่อผู้ใช้หรือที่อยู่อีเมล</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>รหัสผ่าน</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">คลิกที่นี่เพื่อรีเซ็ตรหัสผ่าน</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">ฉันลืมรหัสผ่าน</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">การเข้าสู่ระบบทำให้คุณสามารถเผยแพร่เนื้อหา</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>เข้าสู่ระบบ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">หรือเข้าสู่ระบบด้วย</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>ลืมรหัสผ่าน</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">ขออภัย คุณไม่สามารถกู้คืนรหัสผ่านของคุณเนื่องจากผู้ดูแลระบบไม่ได้ตั้งค่าระบบอีเมล PeerTube</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">ใส่ที่อยู่อีเมลของคุณ เราจะส่งลิงก์เพื่อรีเซ็ตรหัสผ่านของคุณทางอีเมล</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1200,26 +1218,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>อีเมล</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>ที่อยู่อีเมล</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">รีเซ็ต</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">บนเซิร์ฟเวอร์นี้</target>
@@ -1586,9 +1604,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>สร้างบัญชีผู้ใช้</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">วิดีโอของฉัน</target>
@@ -1655,10 +1673,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">วิดีโอ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1737,8 +1755,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">ฉันเป็นกาน้ำชา</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">เกิดข้อผิดพลาด</target>
@@ -1831,8 +1849,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">สื่อมีขนาดใหญ่เกินที่จะอยู่บนเซิร์ฟเวอร์ โปรดติดต่อผู้ดูแลระบบหากคุณต้องการเพิ่มขีดจำกัดขนาด</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">ค้นหาทุกเซิร์ฟเวอร์</target>
@@ -2580,8 +2598,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="new">Upload on hold</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">ขออภัย คุณสมบัติการอัปโหลดถูกปิดใช้งานสำหรับบัญชีของคุณ หากคุณต้องการเพิ่มวิดีโอ ผู้ดูแลระบบต้องปลดล็อกโควต้าของคุณก่อน</target>
@@ -3285,11 +3303,7 @@ The link will expire within 1 hour.</source>
         <target state="new">ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3364,11 +3378,7 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3380,9 +3390,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3393,13 +3403,13 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3409,16 +3419,8 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3468,11 +3470,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>ชื่อผู้ใช้</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3502,9 +3504,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target state="translated">หน้าที่</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3529,15 +3531,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3802,6 +3798,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -4138,8 +4140,8 @@ The link will expire within 1 hour.</source>
         <target state="new">
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -6128,11 +6130,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">ช่อง</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">บัญชีนี้ไม่มีช่อง</target>
@@ -6175,6 +6174,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6737,13 +6742,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="6979021199788941693" datatype="html">
         <source>Your message has been sent.</source>
         <target state="translated">ข้อความของคุณถูกส่งแล้ว</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="translated">คุณเพิ่งส่งฟอร์มนี้ไปเมื่อสักครู่</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">วิดีโอในบัญชี</target>
@@ -6790,13 +6795,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">
           <x id="PH"/> direct account followers
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">รายงานบัญชีนี้</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">วิดีโอ</target>
@@ -6806,31 +6811,21 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">คัดลอกชื่อผู้ใช้แล้ว</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated">ผู้ติดตาม <x id="PH"/> คน</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">เฉพาะเสียง</target>
@@ -6880,6 +6875,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="translated">อัตโนมัติ (ผ่าน ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -7028,18 +7029,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="translated">ไม่จำกัด</target>
@@ -7199,26 +7216,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="new">
           <x id="PH"/> is not valid
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
@@ -7229,8 +7272,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">You are not following 
           <x id="PH"/> anymore.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7721,9 +7764,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="translated">ข้อผิดพลาด</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7768,16 +7811,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7817,16 +7852,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7857,6 +7884,24 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
@@ -8166,13 +8211,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1137937154872046253" datatype="html">
         <source>Video channel <x id="PH"/> created.</source>
         <target state="translated">ช่องวิดีโอ <x id="PH"/> ถูกสร้างแล้ว</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="translated">ชื่อนี้มีอยู่ในเซิร์ฟเวอร์นี้แล้ว</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="translated">อัปเดตช่องวิดีโอ <x id="PH"/> แล้ว</target>
@@ -8193,11 +8238,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Banner deleted.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="translated">กรุณาพิมพ์ชื่อแสดงของช่อง ( <x id="PH"/>) เพื่อยืนยัน</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="translated">ลบช่องวิดีโอ <x id="PH"/> แล้ว</target>
@@ -8361,6 +8402,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="translated">ส่งคำขอเปลี่ยนเจ้าของแล้ว</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8463,7 +8510,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="translated">ติดตามบัญชี</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">เพลย์ลิสต์</target>
@@ -8510,35 +8557,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="translated">ไปที่การติดตามของฉัน</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="translated">ไปที่วิดีโอของฉัน</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="translated">ไปที่การนำเข้าของฉัน</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="translated">ไปที่ช่องของฉัน</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="translated">คุณต้องเชื่อมต่อใหม่</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="translated">ปุ่มลัดคีย์บอร์ด:</target>
@@ -8551,6 +8598,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8579,38 +8632,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target state="translated">ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">บัญชีของคุณถูกบล็อก</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">ภาษาใดก็ได้</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">ซ่อน</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">เบลอ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">แสดง</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">ไม่รู้จัก</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="translated">รีเซ็ตรหัสผ่านเรียบร้อย</target>
@@ -10217,18 +10270,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070" datatype="html">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target state="translated">พยายามหลายครั้งติดต่อกัน โปรดลองอีกครั้งในอีก <x id="PH"/> นาที</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="translated">พยายามหลายครั้งติดต่อกัน โปรดลองอีกครั้งในภายหลัง</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="translated">เซิร์ฟเวอร์เกิดข้อผิดพลาด โปรดลองอีกครั้งในภายหลัง</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">ติดตามทุกช่องในปัจจุบันของ <x id="PH"/> แล้ว คุณจะได้รับการแจ้งเตือนสำหรับวิดีโอใหม่ทุกวิดีโอ</target>
@@ -10826,35 +10879,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275" datatype="html">
         <source>Your video was uploaded to your account and is private.</source>
         <target state="translated">วิดีโอของคุณถูกอัปโหลดไปยังบัญชีของคุณและเป็นส่วนตัวแล้ว</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="translated">แต่ข้อมูลที่เกี่ยวข้อง (เช่น แท็ก คำอธิบาย) จะไม่ถูกบันทึก คุณแน่ใจว่าต้องการออกจากหน้านี้หรือไม่</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="translated">วิดีโอยังไม่ถูกอัปโหลด คุณแน่ใจว่าต้องการออกจากหน้านี้หรือไม่</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">อัปโหลด</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="translated">อัปโหลด 
           <x id="PH"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="translated">เผยแพร่วิดีโอแล้ว</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119" datatype="html">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target state="translated">คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก ถ้าคุณออกจากหน้านี้ ข้อมูลที่ไม่ได้บันทึกจะหายไป</target>
@@ -10901,28 +10954,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">วิดีโอนี้ไม่สามารถรับชมบนเซิร์ฟเวอร์นี้ คุณต้องการเปลี่ยนเส้นทางไปยังเซิร์ฟเวอร์ต้นทางหรือไม่?: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a></target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">การเปลี่ยนเส้นทาง</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="translated">วิดีโอนี้มีเนื้อหาไม่เหมาะสม คุณต้องการรับชมหรือไม่</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="translated">เนื้อหาไม่เหมาะสม</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">รายการถัดไป</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">ยกเลิก</target>
@@ -10931,63 +10984,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">การเล่นวิดีโออัตโนมัติถูกหยุด</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="new">Enter/exit fullscreen (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="new">Play/Pause the video (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="new">Mute/unmute the video (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="translated">ชอบวิดีโอ</target>
index c42282c5dbdf7a3a6f932501af5e112e65db937d..b43dbb444a7b04d72e672aeb8dcc4a0a64073dab 100644 (file)
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">İzleme geçmişim</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">video</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Video sınırı</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="new">Unlimited 
         <source>Federation</source>
         <target state="new">Federation</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="new">followers</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702" datatype="html">
         <source>Ban this user</source>
         <target state="translated">Bu kullanıcıyı yasakla</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Kullanıcı</target>
         <source>Username or email address</source>
         <target>Kullanıcı adı ya da e-posta adresi</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Şifre</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Şifrenizi sıfırlamak için buraya tıklayın</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Şifremi unuttum</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Hesabınıza giriş yapmak içerik yayınlamanızı sağlar</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Oturum aç</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Ya da şununla oturum aç</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367" datatype="html">
         <source>Forgot your password</source>
         <target state="translated">Şifrenizi mi unuttunuz?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="new">We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">E-posta adresinizi girin, şifrenizi sıfırlamak için bir bağlantı göndereceğiz.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1108,26 +1126,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664" datatype="html">
         <source>Email</source>
         <target state="translated">E-posta</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610" datatype="html">
         <source>Email address</source>
         <target state="translated">E-posta adresi</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Sıfırla</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
@@ -1466,7 +1484,7 @@ The link will expire within 1 hour.</target>
         <target state="translated">Hesap oluştur</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1523,7 +1541,7 @@ The link will expire within 1 hour.</target>
         <source>VIDEOS</source>
         <target state="translated">VİDEOLAR</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit><trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source><target state="new">Import jobs concurrency</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">225</context></context-group></trans-unit><trans-unit id="2184839376696112704" datatype="html">
@@ -1597,7 +1615,7 @@ The link will expire within 1 hour.</target>
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Bu bir hata.</target>
@@ -1681,8 +1699,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2376,7 +2394,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3089,11 +3107,7 @@ The link will expire within 1 hour.</target>
         <target state="new">ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3168,11 +3182,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="new">Redundancy allowed 
@@ -3185,7 +3195,7 @@ The link will expire within 1 hour.</target>
         <source>Unfollow</source>
         <target state="translated">Takipten çık</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3197,12 +3207,12 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="new">Showing 
@@ -3212,16 +3222,8 @@ The link will expire within 1 hour.</target>
         </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3268,11 +3270,11 @@ The link will expire within 1 hour.</target>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Kullanıcı adı</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3302,9 +3304,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target state="new">Role</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3327,15 +3329,9 @@ The link will expire within 1 hour.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">172</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/users/user-quota.component.html</context><context context-type="linenumber">13</context></context-group></trans-unit><trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source><target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit><trans-unit id="588099657508661970" datatype="html">
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit><trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source><target state="new">None (local authentication)</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
@@ -3592,7 +3588,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target state="new">Commented video</target>
@@ -3917,7 +3919,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5838,11 +5840,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5882,7 +5881,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="translated">Kanallarım</target>
@@ -6466,12 +6471,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
@@ -6519,12 +6524,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Bu hesabı ihbar et</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       
       <trans-unit id="1504521795586863905" datatype="html">
@@ -6539,27 +6544,19 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="translated">Kullanıcı adı kopyalandı</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 abone</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> abone</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Yalnızca ses</target>
@@ -6609,7 +6606,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="translated">Kendiliğinden (ffmpeg ile)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group></trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
         <target state="translated">Sınır yok</target>
@@ -6741,18 +6744,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="translated">Sınırsız</target>
@@ -6902,26 +6921,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="new">
           <x id="PH"/> is not valid
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="new">Do you really want to unfollow 
           <x id="PH"/>?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Takipten çık</target>
@@ -6932,8 +6977,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">You are not following 
           <x id="PH"/> anymore.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7392,9 +7437,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="translated">Hata</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7439,16 +7484,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7488,16 +7525,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7528,7 +7557,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -7836,12 +7883,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> created.
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="new">Video channel 
@@ -7858,13 +7905,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="new">Video channel 
@@ -8025,6 +8066,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8130,7 +8177,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="translated">Hesaba abone olundu</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -8176,35 +8223,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="translated">Aboneliklerime git</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="translated">Videolarıma git</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="translated">Kanallarıma git</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>Klavye Kısayolları:</target>
@@ -8215,6 +8262,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8238,38 +8291,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target state="translated">Hatalı kullanıcı adı ya da şifre.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Hesabınız engellenmiş.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">herhangi bir dil</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">gizle</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">bulanıklaştır</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">göster</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">Bilinmiyor</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="translated">Şifreniz başarıyla sıfırlandı!</target>
@@ -9833,18 +9886,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target state="new">Too many attempts, please try again after 
           <x id="PH"/> minutes.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10439,35 +10492,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="new">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="new">Upload 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="translated">Video yayınlandı.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10535,27 +10588,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="new">Cancel</target>
@@ -10565,62 +10618,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="new">Autoplay is suspended</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Tam ekrana gir/ekrandan çık (oynatıcı odağı gerekli)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Videoyu oynat/durdur (oynatıcı odağı gerekli)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Videonun sesini aç/kapat (oynatıcı odağı gerekli)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="translated">Videoyu beğen</target>
index b3edca5d1ed6cf97ce74c0e17784a248090f0526..0315524540cacefd465f51270fb5bb5d8b452650 100644 (file)
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Моя історія перегляду</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">відео</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit><trans-unit id="6438815964972582865" datatype="html">
         <source> The following link contains a private token and should not be shared with anyone. </source><target state="new"> The following link contains a private token and should not be shared with anyone. </target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">19</context></context-group></trans-unit><trans-unit id="187187500641108332" datatype="html">
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">289</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit><trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source><target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit><trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source><target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Квота відео</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="translated">Без обмежень <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> на день)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <source>Federation</source>
         <target state="translated">Федерація</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         <target state="translated">підписники</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Заблокувати цього користувача</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="new"> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Користувач</target>
         <source>Username or email address</source>
         <target>Ім'я користувача або email</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Пароль</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Натисніть, щоб скинути ваш пароль</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="new">I forgot my password</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="new"> Logging into an account lets you publish content </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966" datatype="html">
         <source>Login</source>
         <target state="translated">Увійти за допомогою імені</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Або увійти за допомогою</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367" datatype="html">
         <source>Forgot your password</source>
         <target state="translated">Не пам'ятаю пароль</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Вибачте, але ви не можете відновити пароль, тому що адміністратор вашого сервера не налаштував систему надсилання електронних листів PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="new"> Enter your email address and we will send you a link to reset your password. </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1006,26 +1024,26 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4768749765465246664" datatype="html">
         <source>Email</source>
         <target state="translated">Email</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610" datatype="html">
         <source>Email address</source>
         <target state="translated">Адреса електронної пошти</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="new">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
@@ -1373,7 +1391,7 @@ The link will expire within 1 hour.</target>
         <target state="translated">Створити обліковий запис</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
@@ -1430,7 +1448,7 @@ The link will expire within 1 hour.</target>
         <source>VIDEOS</source>
         <target state="translated">ВІДЕО</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="new">Import jobs concurrency</target>
@@ -1510,7 +1528,7 @@ The link will expire within 1 hour.</target>
         <source>I'm a teapot</source>
         <target state="new">I'm a teapot</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="new">That's an error.</target>
@@ -1594,8 +1612,8 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="new">Media is too large for the server. Please contact you administrator if you want to increase the limit size.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2306,7 +2324,7 @@ The link will expire within 1 hour.</target>
       <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">106</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/header/header.component.html</context><context context-type="linenumber">5</context></context-group></trans-unit><trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source><target state="new">Upload on hold</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="new">Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</target>
@@ -3047,11 +3065,7 @@ The link will expire within 1 hour.</target>
         <target state="new">ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="new">Follower handle</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group></trans-unit>
+      
       <trans-unit id="5911214550882917183" datatype="html">
         <source>State</source>
         <target state="new">State</target>
@@ -3119,11 +3133,7 @@ The link will expire within 1 hour.</target>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="new">Host</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group></trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Надлишковість дозволена <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3133,7 +3143,7 @@ The link will expire within 1 hour.</target>
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="new">Open instance in a new tab</target>
@@ -3145,27 +3155,19 @@ The link will expire within 1 hour.</target>
         <source>No host found matching current filters.</source>
         <target state="new">No host found matching current filters.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="new">Your instance is not following anyone.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Показано <x id="INTERPOLATION"/> до <x id="INTERPOLATION_1"/> з <x id="INTERPOLATION_2"/> хостів</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group></trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="new">Follow domains</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group></trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="new">Follow instances</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group></trans-unit><trans-unit id="9216117865911519658" datatype="html">
+      
+      <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source><target state="new">Action</target>
         
         
@@ -3212,11 +3214,11 @@ The link will expire within 1 hour.</target>
       <trans-unit id="5248717555542428023" datatype="html">
         <source>Username</source>
         <target state="new">Username</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="new">e.g. jane_doe</target>
@@ -3246,9 +3248,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="4145496584631696119" datatype="html">
         <source>Role</source>
         <target state="new">Role</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="new">
@@ -3273,15 +3275,9 @@ The link will expire within 1 hour.</target>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="new">None (local authentication)</target>
@@ -3530,7 +3526,13 @@ The link will expire within 1 hour.</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">23</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-block-list/video-block-list.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group></trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
         <target state="new">Commented video</target>
@@ -3826,7 +3828,7 @@ The link will expire within 1 hour.</target>
       It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
     </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="new">Mute domains</target>
@@ -5773,11 +5775,8 @@ color: red;
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.html</context><context context-type="linenumber">80</context></context-group></trans-unit><trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source><target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
@@ -5813,7 +5812,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group></trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
         <target state="new">My Channels</target>
@@ -6404,12 +6409,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target state="new">Your message has been sent.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="new">You already sent this form recently</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
@@ -6457,12 +6462,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> direct account followers
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       
       
       <trans-unit id="1504521795586863905" datatype="html">
@@ -6477,27 +6482,19 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="translated">Ім'я користувача скопійовано</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 підписник</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> підписники</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group></trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6547,6 +6544,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target state="new">Auto (via ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6689,18 +6692,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="new">Unlimited</target>
@@ -6860,24 +6879,50 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> removed from instance followers
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="new">
           <x id="PH"/> is not valid
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="new">Follow request(s) sent!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="translated">Справді відписатися від <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="new">Unfollow</target>
@@ -6886,8 +6931,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3935234189109112926" datatype="html">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target state="translated">Ви більше не стежите за <x id="PH"/>.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="new">enabled</target>
@@ -7345,9 +7390,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="new">Error</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7388,16 +7433,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Update user password</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Користувача <x id="PH"/> онвлено.</target>
@@ -7435,16 +7472,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7475,7 +7504,25 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Set Email as Verified</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group></trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <target state="new">You cannot ban root.</target>
@@ -7783,12 +7830,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Video channel <x id="PH"/> created.</source>
         <target state="translated">Відеоканал <x id="PH"/> створено.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="new">This name already exists on this instance.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="translated">Відеоканал <x id="PH"/> оновлено.</target>
@@ -7803,13 +7850,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Banner deleted.</source><target state="new">Banner deleted.</target>
         
       <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group></trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="new">Please type the display name of the video channel (
-          <x id="PH"/>) to confirm
-        </target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="translated">Відеоканал <x id="PH"/> видалено.</target>
@@ -7954,6 +7995,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target state="new">Ownership change request sent.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8057,7 +8104,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Subscribe to the account</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit><trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source><target state="new">PLAYLISTS</target>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
@@ -8103,35 +8150,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="new">Go to my subscriptions</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="new">Go to my videos</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="new">Go to my imports</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="new">Go to my channels</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="new">You need to reconnect.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="new">Keyboard Shortcuts:</target>
@@ -8142,6 +8189,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit><trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source><target state="new">Trending</target>
         
@@ -8165,38 +8218,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Incorrect username or password.</source>
         <target state="new">Incorrect username or password.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="new">Your password has been successfully reset!</target>
@@ -9775,18 +9828,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070" datatype="html">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target state="translated">Забагато спроб, повторіть спробу через <x id="PH"/> хвилин.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <target state="new">Too many attempts, please try again later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <target state="new">Server error. Please retry later.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10378,35 +10431,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Your video was uploaded to your account and is private.</source>
         <target state="new">Your video was uploaded to your account and is private.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target state="new">But associated data (tags, description...) will be lost, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target state="new">Your video is not uploaded yet, are you sure you want to leave this page?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="new">Upload</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH"/> </source>
         <target state="new">Upload 
           <x id="PH"/>
         </target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <target state="new">Video published.</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       
       
       <trans-unit id="764164089183618119" datatype="html">
@@ -10456,27 +10509,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="new">This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="new">Redirection</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target state="new">This video contains mature or explicit content. Are you sure you want to watch it?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <target state="new">Mature or explicit content</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="new">Up Next</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Скасувати</target>
@@ -10486,62 +10539,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="translated">Автовідтворення зупинено</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Вхід/вихід до повноекранного режиму (фокус повинен бути на програвачі)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Відтворення/пауза відео (фокус повинен бути на програвачі)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Увімкнути/вимкнути звук відео (фокус повинен бути на програвачі)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="new">Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="new">Increase the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="new">Decrease the volume (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="new">Seek the video forward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="new">Seek the video backward (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="new">Increase playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="new">Decrease playback rate (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="new">Navigate in the video frame by frame (requires player focus)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184" datatype="html">
         <source>Like the video</source>
         <target state="new">Like the video</target>
index 301570458844c835422cef9a6ddd0a8d9746a600..4736e3107427d866165aee7ca5aa5d0a74967785 100644 (file)
       <trans-unit id="187187500641108332" datatype="html">
         <source><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action.label }}"/> </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">Lịch sử xem</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>Tạo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">video</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">Đường dẫn chứa một token riêng tư và không nên chia sẻ với bất cứ ai.</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">Bạn đã dùng hết dung lượng cho phép với video này (dung lượng video: <x id="PH" equiv-text="videoSizeBytes"/>, đã dùng: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, dung lượng cho phép: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">Bạn đã dùng hết dung lượng hàng ngày cho phép với video này (dung lượng video: <x id="PH" equiv-text="videoSizeBytes"/>, đã dùng: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, dung lượng cho phép: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">phụ đề</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>Dung lượng</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144" datatype="html">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target state="translated">Không giới hạn <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> mỗi ngày)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target state="translated">Liên hợp</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
       <trans-unit id="2159130950882492111">
         <source>Cancel</source>
         <target>Huỷ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>Chặn người dùng này</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Máy chủ này cho phép đăng ký. Tuy nhiên, hãy cẩn thận đọc kỹ <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Điều khoản dịch vụ<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Điều khoản dịch vụ<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> trước khi tạo tài khoản. Bạn cũng có thể tham khảo thêm một số máy chủ khác tại: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">Máy chủ này đã tắt đăng ký, bạn hãy đọc <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Điều khoản dịch vụ<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> để tìm hiểu thêm hoặc tìm một máy chủ khác cho phép bạn tạo tài khoản và đăng video. Danh sách những máy chủ khác: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>Tài khoản</target>
         <source>Username or email address</source>
         <target>Tên người dùng hoặc địa chỉ email</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>Mật khẩu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">Click vào đây để reset mật khẩu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">Quên mật khẩu</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">Chỉ có thể đăng video sau khi đăng nhập</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>Đăng nhập</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">Hoặc đăng nhập bằng</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>Quên mật khẩu</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">Rất tiếc, bạn không thể reset mật khẩu bởi vì quản trị viên máy chủ không thiết lập hệ thống email PeerTube.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">Nhập email của bạn và chúng tôi sẽ gửi một đường link reset mật khẩu.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1097,26 +1115,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>Thư điện tử</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>Địa chỉ thư điện tử</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">Reset</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">trên máy chủ này</target>
@@ -1452,9 +1470,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>Tạo tài khoản</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">Video của tôi</target>
@@ -1521,10 +1539,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDEO</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">Nhập công việc đồng thời</target>
@@ -1603,8 +1621,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">Tôi là ấm trà</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">Đây là lỗi.</target>
@@ -1697,8 +1715,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">Video có dung lượng quá lớn. Hãy liên hệ quản trị viên nếu bạn muốn tăng giới hạn dung lượng.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">TÌM KIẾM TOÀN CẦU</target>
@@ -2438,8 +2456,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="translated">Đang tiếp tục tải lên</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">Xin lỗi, tài khoản của bạn đã bị cấm tải lên. Nếu bạn muốn đăng thêm video, bạn phải liên hệ một quản trị viên để mở khóa dung lượng cho phép.</target>
@@ -3115,11 +3133,7 @@ The link will expire within 1 hour.</source>
         <target>Mã</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
-        <target state="translated">Người theo dõi handle</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>Trạng thái</target>
@@ -3185,11 +3199,7 @@ The link will expire within 1 hour.</source>
         <target state="translated"><x id="INTERPOLATION" equiv-text="{{ action }}"/> </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <target state="translated">Host</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">Đã cho phép dư thừa <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3198,9 +3208,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Ngưng theo dõi</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">Mở máy chủ trong tab mới</target>
@@ -3211,28 +3221,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">Không tìm thấy host trùng khớp với bộ lọc.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">Máy chủ của bạn không theo dõi bất kỳ ai.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">Đang hiện <x id="INTERPOLATION"/> tới <x id="INTERPOLATION_1"/> của <x id="INTERPOLATION_2"/> hosts</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">Theo dõi tên miền</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">Theo dõi những máy chủ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">Hành động</target>
@@ -3282,11 +3284,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>Tên đăng nhập</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">e.g. jane_doe</target>
@@ -3314,9 +3316,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>Vai trò</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">Đã bật chuyển đổi độ phân giải. Giới hạn dung lượng chỉ áp dụng vào tài khoản <x id="START_TAG_STRONG"/>gốc<x id="CLOSE_TAG_STRONG"/> thước video. <x id="LINE_BREAK"/> Dù vậy, tài khoản vẫn có thể upload ~ <x id="INTERPOLATION"/>. </target>
@@ -3333,15 +3335,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">Plugin cho phép</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">Không (xác thực cục bộ)</target>
@@ -3595,6 +3591,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3896,8 +3898,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">Bạn đang ở trên một máy chủ không hỗ trợ HTTPS. Cần phải kích hoạt TLS trước khi theo dõi những máy khác.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">Ẩn máy chủ</target>
@@ -5832,11 +5834,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">KÊNH</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239" datatype="html">
         <source>This account does not have channels.</source>
         <target state="translated">Tài khoản này không mở kênh.</target>
@@ -5875,6 +5874,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">Bạn có chắc chắn muốn xóa <x id="PH" equiv-text="videoChannel.displayName"/>? Điều này sẽ xóa hết <x id="PH_1" equiv-text="videoChannel.videosCount"/> video đã đăng trên kênh này, và bạn sẽ không thể tạo kênh khác có cùng tên (<x id="PH_2" equiv-text="videoChannel.name"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6388,13 +6393,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="6979021199788941693" datatype="html">
         <source>Your message has been sent.</source>
         <target state="translated">Tin nhắn của bạn đã được gửi đi.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <target state="translated">Bạn đã gửi rồi gần đây</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">Video tài khoản</target>
@@ -6437,13 +6442,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="4856575356061361269" datatype="html">
         <source><x id="PH"/> direct account followers </source>
         <target state="translated"><x id="PH"/> người theo dõi tài khoản trực tiếp </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">Báo cáo tài khoản này</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">VIDEO</target>
@@ -6453,31 +6458,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533" datatype="html">
         <source>Username copied</source>
         <target state="translated">Đã chép tên tài khoản</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 người đăng ký</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> người đăng ký</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Những máy chủ bạn theo dõi</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Những máy chủ đang theo dõi bạn</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">Chỉ có âm thanh</target>
@@ -6527,6 +6522,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target state="translated">Tự động (ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6675,18 +6676,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">Yêu cầu tên miền.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">Tên miền đã nhập không đúng.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">Những tên miền đã nhập bị trùng lặp.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244" datatype="html">
         <source>Unlimited</source>
         <target state="translated">Không giới hạn</target>
@@ -6840,22 +6857,48 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source><x id="PH"/> removed from instance followers </source>
         <target state="translated"><x id="PH"/> đã bị xóa khỏi người theo dõi máy chủ </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895" datatype="html">
         <source><x id="PH"/> is not valid </source>
         <target state="translated"><x id="PH"/> vô giá trị </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196" datatype="html">
         <source>Follow request(s) sent!</source>
         <target state="translated">Đã gửi yêu cầu theo dõi!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target state="translated">Bạn có chắc muốn ngưng theo dõi <x id="PH"/>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">Ngưng theo dõi</target>
@@ -6864,8 +6907,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926" datatype="html">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target state="translated">Bạn không còn theo dõi <x id="PH"/> nữa.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
         <source>enabled</source>
         <target state="translated">đã bật</target>
@@ -7329,9 +7372,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <target state="translated">Lỗi</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">Nhật trình chuẩn</target>
@@ -7372,16 +7415,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Đổi mật khẩu người dùng</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">Danh sách đang theo dõi</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">Danh sách người theo dõi</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">Người dùng <x id="PH"/> đã cập nhật.</target>
@@ -7417,16 +7452,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Liên hợp</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">Những máy chủ bạn theo dõi</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">Những máy chủ theo dõi bạn</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">Video sẽ bị xóa, còn bình luận bị hóa đá.</target>
@@ -7457,6 +7484,24 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Cài Email như Xác Thực</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
@@ -7759,13 +7804,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1137937154872046253" datatype="html">
         <source>Video channel <x id="PH"/> created.</source>
         <target state="translated">Kênh video <x id="PH"/> đã tạo.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <target state="translated">Tên này đã có người đăng ký.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536" datatype="html">
         <source>Video channel <x id="PH"/> updated.</source>
         <target state="translated">Kênh video <x id="PH"/> đã cập nhật.</target>
@@ -7786,11 +7831,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Đã xóa ảnh bìa.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target state="translated">Hãy nhập tên hiển thị của kênh video ( <x id="PH"/>) để xác nhận</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195" datatype="html">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target state="translated">Kênh video <x id="PH"/> đã xóa.</target>
@@ -7938,6 +7979,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target state="translated">Đã gửi yêu cầu thay đổi chủ sở hữu.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814" datatype="html">
         <source>My channels</source>
@@ -8036,7 +8083,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">Theo dõi tài khoản này</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">DANH SÁCH PHÁT</target>
@@ -8083,34 +8130,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370" datatype="html">
         <source>Go to my subscriptions</source>
         <target state="translated">Đến lượt đăng ký của tôi</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <target state="translated">Đến trang video của tôi</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <target state="translated">Đến trang video tôi nhập</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <target state="translated">Đến kênh của tôi</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">Không thể truy xuất thông tin đăng nhập ứng dụng khách OAuth: <x id="PH"/>. Hãy chắc rằng bạn đã cấu hình đúng PeerTube (config/ directory), đặc biệt là phần "webserver".</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544" datatype="html">
         <source>You need to reconnect.</source>
         <target state="translated">Bạn cần kết nối lại.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
         <source>Keyboard Shortcuts:</source>
         <target state="translated">Phím tắt:</target>
@@ -8123,6 +8170,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8151,38 +8204,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246" datatype="html">
         <source>Incorrect username or password.</source>
         <target state="translated">Sai tên hoặc mật khẩu.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">Tài khoản của bạn đã bị khóa.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">ngôn ngữ bất kỳ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">ẩn</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">làm mờ</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">hiển thị</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">—</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853" datatype="html">
         <source>Your password has been successfully reset!</source>
         <target state="translated">Bạn đã đổi mật khẩu thành công!</target>
@@ -9760,18 +9813,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target>Bạn đã thực hiện điều này quá nhiều lần, xin thử lại sau 
           <x id="PH"/> phút.
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>Quá nhiều lần thực hiện, vui lòng thử lại sau.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>Lỗi máy chủ. Xin thử lại sau.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">Đã đăng toàn bộ kênh gần đây của <x id="PH"/>. Bạn sẽ nhận được thông báo về video mới của họ.</target>
@@ -10363,35 +10416,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275">
         <source>Your video was uploaded to your account and is private.</source>
         <target>Video đã được tải lên riêng tư vào tài khoản của bạn.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>Nhưng các dữ liệu liên quan (thẻ, mô tả,...) sẽ bị mất. Bạn có chắc muốn rời khỏi trang không?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>Video của bạn vẫn chưa được tải lên, bạn có chắc muốn rời trang?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">Tải lên</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload <x id="PH"/> </source>
         <target>Tải lên 
           <x id="PH"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>Đã xuất bản video.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>Bạn có sửa đổi chưa lưu! Nếu rời đi, những sửa đổi này sẽ bị mất.</target>
@@ -10458,28 +10511,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">Video không khả dụng trên máy chủ này. Bạn có muốn chuyển tới máy chủ gốc: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">Chuyển hướng</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>Video này chứa nội dung cho người lớn hoặc nhạy cảm. Bạn có chắc chắn muốn xem không?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>Nội dung người lớn hoặc nhạy cảm</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">Tiếp Theo</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">Hủy bỏ</target>
@@ -10488,63 +10541,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">Tạm ngừng tự phát</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">Mở/thoát toàn màn hình (yêu cầu trọng tâm trình phát)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">Phát/Ngừng video (yêu cầu trọng tâm trình phát)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">Ẩn/bỏ ẩn video (yêu cầu trọng tâm trình phát)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">Chuyển đến một mức phần trăm của video: 0 là 0% và 9 là 90% (yêu cầu trọng tâm trình phát)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">Tăng âm lượng (yêu cầu trọng tâm trình phát)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">Giảm âm lượng (yêu cầu trọng tâm trình phát)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">Tua tới video (yêu cầu trọng tâm trình phát)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">Tua lùi video (yêu cầu trọng tâm video)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">Tăng tốc độ phát (yêu cầu trọng tâm trình phát)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">Giảm tốc độ phát (yêu cầu trọng tâm trình phát)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">Điều hướng trong từng khung hình video (yêu cầu trọng tâm trình phát)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>Thích video</target>
index 39012827dc6865abba032871c82961e4b01eeaba..418d2fe96fb5de7f94029805c387e1c148d786e8 100644 (file)
           <context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context>
           <context context-type="linenumber">48</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">33</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">117</context>
+          <context context-type="linenumber">121</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">408</context>
+          <context context-type="linenumber">415</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/modal/confirm.component.html</context>
         <source>Your message has been sent.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context>
-          <context context-type="linenumber">89</context>
+          <context context-type="linenumber">88</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2072135752262464360" datatype="html">
         <source>You already sent this form recently</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context>
-          <context context-type="linenumber">95</context>
+          <context context-type="linenumber">94</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1045244999981860085" datatype="html">
         <source>CHANNELS</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
+          <context context-type="linenumber">81</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">83</context>
+          <context context-type="linenumber">82</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context>
         <source>Username copied</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">121</context>
+          <context context-type="linenumber">120</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context>
         <source>1 subscriber</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">125</context>
+          <context context-type="linenumber">124</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH" equiv-text="count"/> subscribers</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">127</context>
+          <context context-type="linenumber">126</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4856575356061361269" datatype="html">
         <source><x id="PH" equiv-text="account.followersCount"/> direct account followers</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">155</context>
+          <context context-type="linenumber">154</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">196</context>
+          <context context-type="linenumber">195</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8564701209009684429" datatype="html">
           <context context-type="linenumber">58</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
+      <trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
           <context context-type="linenumber">29</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
-          <context context-type="linenumber">3</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
+      <trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
           <context context-type="linenumber">34</context>
         </context-group>
         <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
-          <context context-type="linenumber">3</context>
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
         </context-group>
       </trans-unit>
       <trans-unit id="9031514421077169181" datatype="html">
@@ -2968,6 +2976,13 @@ color: red;
           <context context-type="linenumber">50</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="2845798909207198924" datatype="html">
         <source>Showing <x id="INTERPOLATION" equiv-text="{{'{first}'}}"/> to <x id="INTERPOLATION_1" equiv-text="{{'{last}'}}"/> of <x id="INTERPOLATION_2" equiv-text="{{'{totalRecords}'}}"/> followers</source>
         <context-group purpose="location">
@@ -2998,8 +3013,8 @@ color: red;
           <context context-type="linenumber">41</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441" datatype="html">
-        <source>Follower handle</source>
+      <trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
           <context context-type="linenumber">24</context>
@@ -3260,18 +3275,54 @@ color: red;
           <context context-type="linenumber">81</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="4774348799569692380" datatype="html">
-        <source>Showing <x id="INTERPOLATION" equiv-text="{{'{first}'}}"/> to <x id="INTERPOLATION_1" equiv-text="{{'{last}'}}"/> of <x id="INTERPOLATION_2" equiv-text="{{'{totalRecords}'}}"/> hosts</source>
+      <trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
           <context context-type="linenumber">11</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
+      <trans-unit id="4917252294930256268" datatype="html">
+        <source> It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers. </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">28,29</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="2355066641781598196" datatype="html">
+        <source>Follow request(s) sent!</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context>
+          <context context-type="linenumber">62</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
-          <context context-type="linenumber">18</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="4774348799569692380" datatype="html">
+        <source>Showing <x id="INTERPOLATION" equiv-text="{{'{first}'}}"/> to <x id="INTERPOLATION_1" equiv-text="{{'{last}'}}"/> of <x id="INTERPOLATION_2" equiv-text="{{'{totalRecords}'}}"/> hosts</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">11</context>
         </context-group>
       </trans-unit>
       <trans-unit id="9216117865911519658" datatype="html">
@@ -3301,13 +3352,6 @@ color: red;
           <context context-type="linenumber">30</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335" datatype="html">
-        <source>Host</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
-          <context context-type="linenumber">31</context>
-        </context-group>
-      </trans-unit>
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON" ctype="x-p_sorticon" equiv-text="&lt;p-sortIcon field=&quot;redundancyAllowed&quot;>"/><x id="CLOSE_TAG_P_SORTICON" ctype="x-p_sorticon" equiv-text="&lt;/p-sortIcon>"/></source>
         <context-group purpose="location">
@@ -3323,7 +3367,7 @@ color: red;
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context>
-          <context context-type="linenumber">58</context>
+          <context context-type="linenumber">48</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
@@ -3345,63 +3389,28 @@ color: red;
         <source>No host found matching current filters.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
-          <context context-type="linenumber">70</context>
+          <context context-type="linenumber">71</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
-          <context context-type="linenumber">71</context>
-        </context-group>
-      </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
-          <context context-type="linenumber">78</context>
-        </context-group>
-      </trans-unit>
-      <trans-unit id="4917252294930256268" datatype="html">
-        <source> It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers. </source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
-          <context context-type="linenumber">81,82</context>
-        </context-group>
-      </trans-unit>
-      <trans-unit id="2355066641781598196" datatype="html">
-        <source>Follow request(s) sent!</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context>
-          <context context-type="linenumber">47</context>
+          <context context-type="linenumber">72</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482" datatype="html">
         <source>Do you really want to unfollow <x id="PH" equiv-text="follow.following.host"/>?</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context>
-          <context context-type="linenumber">57</context>
+          <context context-type="linenumber">47</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3935234189109112926" datatype="html">
         <source>You are not following <x id="PH" equiv-text="follow.following.host"/> anymore.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context>
-          <context context-type="linenumber">64</context>
-        </context-group>
-      </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
-          <context context-type="linenumber">28</context>
-        </context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
-          <context context-type="linenumber">37</context>
+          <context context-type="linenumber">54</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2593763089859685916" datatype="html">
@@ -4578,7 +4587,7 @@ color: red;
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context>
-          <context context-type="linenumber">103</context>
+          <context context-type="linenumber">102</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context>
@@ -4735,6 +4744,10 @@ color: red;
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
           <context context-type="linenumber">83</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">111</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context>
           <context context-type="linenumber">6</context>
@@ -4791,9 +4804,13 @@ color: red;
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
           <context context-type="linenumber">105</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">112</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">107</context>
+          <context context-type="linenumber">111</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context>
@@ -4835,11 +4852,11 @@ color: red;
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">34</context>
+          <context context-type="linenumber">38</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">36</context>
+          <context context-type="linenumber">40</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context>
@@ -4879,6 +4896,10 @@ color: red;
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
           <context context-type="linenumber">136</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">114</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2602586221576511475" datatype="html">
         <source>Video quota</source>
@@ -4890,6 +4911,10 @@ color: red;
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
           <context context-type="linenumber">151</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">113</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context>
           <context context-type="linenumber">47</context>
@@ -4931,6 +4956,10 @@ color: red;
           <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
           <context context-type="linenumber">188</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">121</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
@@ -5192,6 +5221,27 @@ color: red;
           <context context-type="linenumber">281</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3403978719736970622" datatype="html">
         <source>You cannot ban root.</source>
         <context-group purpose="location">
@@ -5278,7 +5328,7 @@ color: red;
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">44</context>
+          <context context-type="linenumber">48</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/menu/menu.component.html</context>
@@ -5332,25 +5382,32 @@ color: red;
           <context context-type="linenumber">23</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
+          <context context-type="linenumber">51</context>
         </context-group>
       </trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
+          <context context-type="linenumber">51</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2308975396733519902" datatype="html">
         <source>Create an account</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">50</context>
+          <context context-type="linenumber">54</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/menu/menu.component.html</context>
@@ -5361,56 +5418,56 @@ color: red;
         <source> Logging into an account lets you publish content </source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
+          <context context-type="linenumber">60,61</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7252854992688790751" datatype="html">
         <source> This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
+          <context context-type="linenumber">64,66</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source> Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
+          <context context-type="linenumber">69,71</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">72</context>
+          <context context-type="linenumber">76</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3238209155172574367" datatype="html">
         <source>Forgot your password</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">91</context>
+          <context context-type="linenumber">95</context>
         </context-group>
       </trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source> We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system. </source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">99,100</context>
+          <context context-type="linenumber">103,104</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source> Enter your email address and we will send you a link to reset your password. </source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">103,104</context>
+          <context context-type="linenumber">107,108</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3967269098753656610" datatype="html">
         <source>Email address</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">109</context>
+          <context context-type="linenumber">113</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context>
@@ -5421,7 +5478,7 @@ color: red;
         <source>Reset</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">122</context>
+          <context context-type="linenumber">126</context>
         </context-group>
         <note priority="1" from="description">Password reset button</note>
       </trans-unit>
@@ -5437,14 +5494,14 @@ The link will expire within 1 hour.</source>
         <source>Incorrect username or password.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.ts</context>
-          <context context-type="linenumber">159</context>
+          <context context-type="linenumber">163</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+login/login.component.ts</context>
-          <context context-type="linenumber">160</context>
+          <context context-type="linenumber">164</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6658000829978978023" datatype="html">
@@ -6021,14 +6078,14 @@ The link will expire within 1 hour.</source>
         <source>Video channel <x id="PH" equiv-text="videoChannelCreate.displayName"/> created.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context>
-          <context context-type="linenumber">67</context>
+          <context context-type="linenumber">66</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8723777130353305761" datatype="html">
         <source>This name already exists on this instance.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context>
-          <context context-type="linenumber">73</context>
+          <context context-type="linenumber">72</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
@@ -6257,8 +6314,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <context context-type="linenumber">44,46</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736" datatype="html">
-        <source>Please type the display name of the video channel (<x id="PH" equiv-text="videoChannel.displayName"/>) to confirm</source>
+      <trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
           <context context-type="linenumber">48</context>
@@ -6866,6 +6923,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <context context-type="linenumber">64</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="1788875035518441092" datatype="html">
         <source>Last published first</source>
         <context-group purpose="location">
@@ -7049,7 +7113,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>I'm a teapot</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context>
-          <context context-type="linenumber">26</context>
+          <context context-type="linenumber">27</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3851357780293085233" datatype="html">
@@ -7854,7 +7918,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">704</context>
+          <context context-type="linenumber">711</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
@@ -8787,56 +8851,56 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Upload on hold</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context>
-          <context context-type="linenumber">124</context>
+          <context context-type="linenumber">123</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3284171506518522275" datatype="html">
         <source>Your video was uploaded to your account and is private.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context>
-          <context context-type="linenumber">162</context>
+          <context context-type="linenumber">161</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5699822024600815733" datatype="html">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context>
-          <context context-type="linenumber">163</context>
+          <context context-type="linenumber">162</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1219739004043110649" datatype="html">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context>
-          <context context-type="linenumber">165</context>
+          <context context-type="linenumber">164</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context>
-          <context context-type="linenumber">222</context>
+          <context context-type="linenumber">221</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8278735427925094503" datatype="html">
         <source>Upload <x id="PH" equiv-text="videofile.name"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context>
-          <context context-type="linenumber">224</context>
+          <context context-type="linenumber">223</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5981816353437801748" datatype="html">
         <source>Video published.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context>
-          <context context-type="linenumber">245</context>
+          <context context-type="linenumber">244</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context>
-          <context context-type="linenumber">288</context>
+          <context context-type="linenumber">287</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context>
@@ -8847,14 +8911,14 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context>
-          <context context-type="linenumber">323</context>
+          <context context-type="linenumber">322</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context>
-          <context context-type="linenumber">341</context>
+          <context context-type="linenumber">340</context>
         </context-group>
       </trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
@@ -9596,10 +9660,6 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
           <context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context>
           <context context-type="linenumber">24</context>
         </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context>
-          <context context-type="linenumber">3</context>
-        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context>
           <context context-type="linenumber">27</context>
@@ -9629,119 +9689,119 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH" equiv-text="originUrl"/>"><x id="PH_1" equiv-text="originUrl"/>&lt;/a>?</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">288</context>
+          <context context-type="linenumber">295</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">289</context>
+          <context context-type="linenumber">296</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8858527736400081688" datatype="html">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">335</context>
+          <context context-type="linenumber">342</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3937119019020041049" datatype="html">
         <source>Mature or explicit content</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">336</context>
+          <context context-type="linenumber">343</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">407</context>
+          <context context-type="linenumber">414</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">409</context>
+          <context context-type="linenumber">416</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">678</context>
+          <context context-type="linenumber">685</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">679</context>
+          <context context-type="linenumber">686</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">680</context>
+          <context context-type="linenumber">687</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">682</context>
+          <context context-type="linenumber">689</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">684</context>
+          <context context-type="linenumber">691</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">685</context>
+          <context context-type="linenumber">692</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">687</context>
+          <context context-type="linenumber">694</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">688</context>
+          <context context-type="linenumber">695</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">690</context>
+          <context context-type="linenumber">697</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">691</context>
+          <context context-type="linenumber">698</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context>
-          <context context-type="linenumber">693</context>
+          <context context-type="linenumber">700</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7627544798203088407" datatype="html">
@@ -10018,28 +10078,28 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Go to my subscriptions</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/auth/auth.service.ts</context>
-          <context context-type="linenumber">64</context>
+          <context context-type="linenumber">63</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1136469849928650779" datatype="html">
         <source>Go to my videos</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/auth/auth.service.ts</context>
-          <context context-type="linenumber">68</context>
+          <context context-type="linenumber">67</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7836683738999600376" datatype="html">
         <source>Go to my imports</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/auth/auth.service.ts</context>
-          <context context-type="linenumber">72</context>
+          <context context-type="linenumber">71</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7511292153332773503" datatype="html">
         <source>Go to my channels</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/auth/auth.service.ts</context>
-          <context context-type="linenumber">76</context>
+          <context context-type="linenumber">75</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
@@ -10047,14 +10107,14 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/auth/auth.service.ts</context>
-          <context context-type="linenumber">99,100</context>
+          <context context-type="linenumber">98,99</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/auth/auth.service.ts</context>
-          <context context-type="linenumber">104</context>
+          <context context-type="linenumber">103</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context>
@@ -10065,7 +10125,7 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>You need to reconnect.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/auth/auth.service.ts</context>
-          <context context-type="linenumber">220</context>
+          <context context-type="linenumber">219</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2206638022166154361" datatype="html">
@@ -10082,6 +10142,13 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="linenumber">98</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="2821179408673282599" datatype="html">
         <source>Home</source>
         <context-group purpose="location">
@@ -10125,28 +10192,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context>
-          <context context-type="linenumber">62</context>
+          <context context-type="linenumber">61</context>
         </context-group>
       </trans-unit>
       <trans-unit id="968295009933361070" datatype="html">
         <source>Too many attempts, please try again after <x id="PH" equiv-text="minutesLeft"/> minutes.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context>
-          <context context-type="linenumber">67</context>
+          <context context-type="linenumber">66</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4965472196059235310" datatype="html">
         <source>Too many attempts, please try again later.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context>
-          <context context-type="linenumber">69</context>
+          <context context-type="linenumber">68</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1693549688987384699" datatype="html">
         <source>Server error. Please retry later.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context>
-          <context context-type="linenumber">72</context>
+          <context context-type="linenumber">71</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4670312387769733978" datatype="html">
@@ -10487,35 +10554,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Unknown</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/menu/menu.component.ts</context>
-          <context context-type="linenumber">193</context>
+          <context context-type="linenumber">196</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/menu/menu.component.ts</context>
-          <context context-type="linenumber">263</context>
+          <context context-type="linenumber">266</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/menu/menu.component.ts</context>
-          <context context-type="linenumber">298</context>
+          <context context-type="linenumber">301</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/menu/menu.component.ts</context>
-          <context context-type="linenumber">302</context>
+          <context context-type="linenumber">305</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/menu/menu.component.ts</context>
-          <context context-type="linenumber">306</context>
+          <context context-type="linenumber">309</context>
         </context-group>
       </trans-unit>
       <trans-unit id="403762424689874454" datatype="html">
@@ -10842,34 +10909,6 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="linenumber">27</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="2740793005745065895" datatype="html">
-        <source><x id="PH" equiv-text="host"/> is not valid</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context>
-          <context context-type="linenumber">19</context>
-        </context-group>
-      </trans-unit>
-      <trans-unit id="2127446333083057097" datatype="html">
-        <source>Domain is required.</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context>
-          <context context-type="linenumber">56</context>
-        </context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context>
-          <context context-type="linenumber">57</context>
-        </context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context>
-          <context context-type="linenumber">58</context>
-        </context-group>
-      </trans-unit>
       <trans-unit id="7784486624424057376" datatype="html">
         <source>Instance name is required.</source>
         <context-group purpose="location">
@@ -11073,6 +11112,56 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="linenumber">119</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="2740793005745065895" datatype="html">
+        <source><x id="PH" equiv-text="host"/> is not valid</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">27</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">50</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="2127446333083057097" datatype="html">
+        <source>Domain is required.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">92</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">101</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="8602814243662345124" datatype="html">
         <source>Email is required.</source>
         <context-group purpose="location">
index 3a6c603f814b3257430448220410d33e4416a221..1f22387b7cd11baee5f2ee78eef168791d53898a 100644 (file)
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">我的历史记录</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>创建</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">视频</target>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="new"> The following link contains a private token and should not be shared with anyone. </target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="new">Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="new">Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">字幕</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>视频存储空间</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>无限制 <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> 每日)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target state="translated">联盟</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106" datatype="html">
         <source>followers</source>
         
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>封禁此用户</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">此实例允许注册。但是,请小心检查 <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>条款<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>条款<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> 你也可以通过以下链接搜寻其他服务,以配合你的实际需要: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">目前此实例不允许用户注册,您可以检查 <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>条款<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> 以了解详情,或寻找别的站台,好让您注册帐号并上传您的视频。看看一众站台中有哪个合您心意: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>用户</target>
         <source>Username or email address</source>
         <target>用户名或电子邮件地址</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>密码</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">点击此处重置您的密码</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">我忘记了我 密码</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">登录帐户就可以让您发布内容</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>登录</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">或使用其他账户登入</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>忘记密码</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488" datatype="html">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target state="translated">对不起,您无法恢复您的密码,因为您的实例管理员没有配置 PeerTube 电子邮件系统。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">输入您的电子邮件地址,我们将发送一个链接,以重置您的密码。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1098,26 +1116,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>电子邮件地址</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>电子邮件地址</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">重设</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">在此网站</target>
@@ -1446,9 +1464,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>创建帐户</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">我的视频</target>
@@ -1515,10 +1533,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">视频</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">导入并发送</target>
@@ -1596,8 +1614,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">我是茶壶</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">发生错误.</target>
@@ -1690,8 +1708,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">媒体对于服务器太大。如果您想增加限制大小,请与管理员联系。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
@@ -2434,8 +2452,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="new">Upload on hold</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">对不起,您的帐户的上传功能已被禁用。如果你想添加视频,管理员必须解锁您的权限。</target>
@@ -3108,11 +3126,7 @@ The link will expire within 1 hour.</source>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target>管理关注</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>状态</target>
@@ -3183,11 +3197,7 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>主机名</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">允许冗余 <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3196,9 +3206,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">取消关注</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">在新选项卡中打开页面</target>
@@ -3209,28 +3219,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">没有找到匹配当前筛选器的主机。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">您的站点没有跟踪任何人。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">正在观看 <x id="INTERPOLATION"/> 到 <x id="INTERPOLATION_1"/> 总共有 <x id="INTERPOLATION_2"/> 个主机</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">关注域名</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">跟踪站点</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="new">Action</target>
@@ -3280,11 +3282,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>用户名</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">例如: jane_doe</target>
@@ -3312,9 +3314,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>角色</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">已启用转码。视频配额仅考虑在内 <x id="START_TAG_STRONG"/>原始<x id="CLOSE_TAG_STRONG"/> 视频大小。 <x id="LINE_BREAK"/> 这个用户最多可以上传 ~ <x id="INTERPOLATION"/>. </target>
@@ -3331,15 +3333,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="new">Auth plugin</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">None (本地身份验证)</target>
@@ -3593,6 +3589,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3899,8 +3901,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">看起来你不在 HTTPS 服务器上。你的网络服务器需要有 TLS 激活,以便跟踪服务器。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">静音网络</target>
@@ -5902,11 +5904,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="new">CHANNELS</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
         <target>此帐户没有视频频道。</target>
@@ -5951,6 +5950,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
 It will delete <x id="PH_1"/> videos uploaded in this channel, and you will not be able to create another
 channel with the same name (<x id="PH_2"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6494,12 +6499,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Your message has been sent.</source>
         <target>您的信息已发送。</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>您最近已发送了此表格</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="new">Account videos</target>
@@ -6546,13 +6551,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">
           <x id="PH"/> direct account followers
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="new">Report this account</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="new">VIDEOS</target>
@@ -6562,31 +6567,21 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="25349740244798533">
         <source>Username copied</source>
         <target>用户名已复制</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="new">1 subscriber</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="new"><x id="PH"/> subscribers</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="new">Audio-only</target>
@@ -6636,6 +6631,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Auto (via ffmpeg)</source>
         <target>自动(由 ffmpeg 决定)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6786,18 +6787,34 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="new">Domain is required.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="new">Domains entered are invalid.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="new">Domains entered contain duplicates.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>无限制
@@ -6958,26 +6975,52 @@ channel with the same name (<x id="PH_2"/>)!</target>
           <x id="PH"/> 已被移除出关注列表
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> 不合法
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>关注请求已发送!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>您确定要取消关注 
           <x id="PH"/> 吗?
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>取消关注</target>
@@ -6988,8 +7031,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>您已不再关注 
           <x id="PH"/>。
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>已启用</target>
@@ -7480,9 +7523,9 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>错误</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="new">Standard logs</target>
@@ -7527,16 +7570,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>更改用户密码</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="new">Following list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="new">Followers list</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="new">User 
@@ -7576,16 +7611,8 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Federation</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="new">Instances you follow</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="new">Instances following you</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="new">Videos will be deleted, comments will be tombstoned.</target>
@@ -7616,6 +7643,24 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>把电子邮件地址设为已验证</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
@@ -7929,13 +7974,13 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>视频频道 
           <x id="PH"/> 已创建。
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>此用户名在本实例上已经被使用过。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>视频频道 
@@ -7958,13 +8003,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target state="new">Banner deleted.</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>输入视频频道的显示名(
-          <x id="PH"/>)以确认操作
-        </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>视频频道 
@@ -8126,6 +8165,12 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <source>Ownership change request sent.</source>
         <target>视频转移请求已发送。</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8228,7 +8273,7 @@ channel with the same name (<x id="PH_2"/>)!</target>
         <target>订阅此帐户</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">播放列表</target>
@@ -8275,35 +8320,35 @@ channel with the same name (<x id="PH_2"/>)!</target>
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>转到我的订阅</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>转到我的视频</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>转到我的导入</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>转到我的频道</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="new">Cannot retrieve OAuth Client credentials: <x id="PH"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>请重新进行授权。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>键盘快捷键:</target>
@@ -8316,6 +8361,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8344,38 +8395,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>用户名或密码不正确。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="new">Your account is blocked.</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="new">any language</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="new">hide</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="new">blur</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="new">display</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="new">Unknown</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>密码重置成功!</target>
@@ -9986,18 +10037,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <target>尝试次数过多,请在 
           <x id="PH"/> 分钟后重试。
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>尝试次数过多,请稍后重试。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>服务器出现错误。请稍后重试。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="new">Subscribed to all current channels of 
@@ -10593,35 +10644,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275">
         <source>Your video was uploaded to your account and is private.</source>
         <target>您的视频已经以私有方式上传至您的帐户。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>相关信息(如标签、说明)将会丢失,您确定要离开这个页面吗?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>您的视频尚未上传完毕,您确定要离开这个页面吗?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">上传</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload <x id="PH"/> </source>
         <target>上传 
           <x id="PH"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>视频已发布。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>您有未保存的修改!如果您离开本页面,您将会失去这些修改。</target>
@@ -10689,27 +10740,27 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">此视频在此实例上不可用,是否要在原始实例上重定向: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">重新导向</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>此视频包含成人或裸露内容。您确定要观看吗?</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>成人或裸露内容</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">往下</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">取消</target>
@@ -10719,62 +10770,62 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         <source>Autoplay is suspended</source>
         <target state="translated">自动播放已经暂停</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">进入/离开全屏幕(需要播放器聚焦)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">播放/暂停视频(需要播放器聚焦)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">静音/解除视频静音(需要播放器焦点)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">跳到视频的百分比:0 是 0%,9 是 90%(需要播放器焦点)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">增加音量(需要播放器焦点)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">降低音量(需要播放器焦点)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">快进视频(需要播放器焦点)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">向后快退视频(需要播放器焦点點)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">提高播放速度(需要播放器点击)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">减慢播放速度(需要播放器点击)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">逐画格浏览视频(需要点击播放器)</target>
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target state="translated">喜欢这视频</target>
index d73c743f4a0c44d60232bc54dfa7b7c8edc98810..1f5036e7d7c3dbc86afd8fb77d5ebee9db9dabea 100644 (file)
         <target state="translated">
           <x id="INTERPOLATION" equiv-text="{{ action.label }}"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.html</context><context context-type="linenumber">77</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/buttons/action-dropdown.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">14</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-main/misc/top-menu-dropdown.component.html</context><context context-type="linenumber">24</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">52</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">78</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">101</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/videos-selection.component.html</context><context context-type="linenumber">1</context></context-group></trans-unit>
       <trans-unit id="1486537403020619891" datatype="html">
         <source>My watch history</source>
         <target state="translated">我的觀看紀錄</target>
       <trans-unit id="5674286808255988565">
         <source>Create</source>
         <target>建立</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">103</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">102</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts</context><context context-type="linenumber">89</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-playlist/video-add-to-playlist.component.html</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="1006562256968398209" datatype="html">
         <source>video</source>
         <target state="translated">影片</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">288</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">287</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.ts</context><context context-type="linenumber">55</context></context-group></trans-unit>
       <trans-unit id="6438815964972582865" datatype="html">
         <source>The following link contains a private token and should not be shared with anyone.</source>
         <target state="translated">以下連結包含了一個專用權杖,不應該與其他人分享。</target>
       <trans-unit id="6995024616159044376" datatype="html">
         <source>Your video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="videoQuotaUsedBytes"/>, quota: <x id="PH_2" equiv-text="videoQuotaBytes"/>)</source>
         <target state="translated">此影片超過了您的影片配額(影片大小:<x id="PH" equiv-text="videoSizeBytes"/>,已使用:<x id="PH_1" equiv-text="videoQuotaUsedBytes"/>,配額:<x id="PH_2" equiv-text="videoQuotaBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">323</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">322</context></context-group></trans-unit>
       <trans-unit id="7873395933409147217" datatype="html">
         <source>Your daily video quota is exceeded with this video (video size: <x id="PH" equiv-text="videoSizeBytes"/>, used: <x id="PH_1" equiv-text="quotaUsedDailyBytes"/>, quota: <x id="PH_2" equiv-text="quotaDailyBytes"/>)</source>
         <target state="translated">此影片超過了您的每日影片配額(影片大小:<x id="PH" equiv-text="videoSizeBytes"/>,已使用:<x id="PH_1" equiv-text="quotaUsedDailyBytes"/>,配額:<x id="PH_2" equiv-text="quotaDailyBytes"/>)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">341</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">340</context></context-group></trans-unit>
       <trans-unit id="5235042777215655908" datatype="html">
         <source>subtitles</source>
         <target state="translated">字幕</target>
       <trans-unit id="2602586221576511475">
         <source>Video quota</source>
         <target>影片配額</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">151</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-features-table.component.html</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="1502595455339510144">
         <source>Unlimited <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/> per day)<x id="CLOSE_TAG_NG_CONTAINER"/></source>
         <target>無限 <x id="START_TAG_NG_CONTAINER"/>(<x id="INTERPOLATION"/>每日)<x id="CLOSE_TAG_NG_CONTAINER"/></target>
         <target>聯盟</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-instance/instance-statistics.component.html</context><context context-type="linenumber">58</context></context-group>
+      </trans-unit><trans-unit id="8726138323871139597" datatype="html">
+        <source>Following</source><target state="new">Following</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">31</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="4914577418256256836" datatype="html">
+        <source>Followers</source><target state="new">Followers</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/admin.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3541687134897970106">
         <source>followers</source>
       <trans-unit id="2159130950882492111">
         <source>Cancel</source>
         <target>取消</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">408</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">33</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">121</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.html</context><context context-type="linenumber">22</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html</context><context context-type="linenumber">37</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">69</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.html</context><context context-type="linenumber">81</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html</context><context context-type="linenumber">73</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">415</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/modal/confirm.component.html</context><context context-type="linenumber">20</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/moderation-comment-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">31</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/report.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/report-modals/video-report.component.html</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-ban-modal.component.html</context><context context-type="linenumber">26</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/video-block.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-video-miniature/video-download.component.html</context><context context-type="linenumber">152</context></context-group></trans-unit>
       <trans-unit id="3616223838716839702">
         <source>Ban this user</source>
         <target>阻擋此使用者</target>
       <trans-unit id="7252854992688790751" datatype="html">
         <source>This instance allows registration. However, be careful to check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> before creating an account. You may also search for another instance to match your exact needs at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">此站臺允許註冊。然而,請留心查閱<x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;terms-anchor&quot; (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>條款<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;terms-link&quot; target=&quot;_blank&quot; routerLink=&quot;/about/instance&quot; fragment=&quot;terms&quot;>"/>條款<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> ,然後才建立帳號。您亦可搜尋另一個站臺以切合您的需要:<x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br />"/><x id="START_LINK_2" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>。 </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">60,62</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">64</context></context-group></trans-unit>
       <trans-unit id="7215649348148521605" datatype="html">
         <source>Currently this instance doesn't allow for user registration, you may check the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>Terms<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there. Find yours among multiple instances at: <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>. </source>
         <target state="translated">目前此站臺不允許使用者註冊,您可查閱<x id="START_LINK" ctype="x-a" equiv-text="&lt;a (click)=&quot;onTermsClick($event, instanceInformation)&quot; href='#'>"/>條款<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/> 以瞭解詳情,或尋找別的站臺,好讓您註冊帳號並上載您的影片。看看一眾站臺中有哪個合您心意:<x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br /> "/><x id="START_LINK_1" equiv-text="&lt;a class=&quot;alert-link&quot; href=&quot;https://joinpeertube.org/instances&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"/>https://joinpeertube.org/instances<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a>"/>。 </target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">65,67</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">69</context></context-group></trans-unit>
       <trans-unit id="2392488717875840729">
         <source>User</source>
         <target>使用者</target>
         <source>Username or email address</source>
         <target>使用者名稱或電子信箱</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">23</context></context-group>
+      </trans-unit><trans-unit id="1758058452376026925" datatype="html">
+        <source> ⚠️ Most email addresses do not include capital letters. </source><target state="new"> ⚠️ Most email addresses do not include capital letters. </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+login/login.component.html</context>
+          <context context-type="linenumber">33,34</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="1431416938026210429">
         <source>Password</source>
         <target>密碼</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">36</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">117</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">38</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">40</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">8</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+reset-password/reset-password.component.html</context><context context-type="linenumber">10</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">56</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">58</context></context-group></trans-unit>
       <trans-unit id="8715156686857791956" datatype="html">
         <source>Click here to reset your password</source>
         <target state="translated">點擊此處以重設您的密碼</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">47</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="892063502898494584" datatype="html">
         <source>I forgot my password</source>
         <target state="translated">我忘了我的密碼</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">47</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">51</context></context-group></trans-unit>
       <trans-unit id="2101170466365500913" datatype="html">
         <source>Logging into an account lets you publish content</source>
         <target state="translated">登入帳號就可讓您發佈內容</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+login/login.component.html</context>
-          <context context-type="linenumber">56,57</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">60</context></context-group></trans-unit>
       <trans-unit id="2454050363478003966">
         <source>Login</source>
         <target>登入</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">44</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login-routing.module.ts</context><context context-type="linenumber">12</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">48</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">99</context></context-group></trans-unit>
       <trans-unit id="3183213940445113677" datatype="html">
         <source>Or sign in with</source>
         <target state="translated">或使用其他帳戶登入</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="3238209155172574367">
         <source>Forgot your password</source>
         <target>忘記您的密碼</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">91</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">95</context></context-group></trans-unit>
       <trans-unit id="87327320394367488">
         <source>We are sorry, you cannot recover your password because your instance administrator did not configure the PeerTube email system.</source>
         <target>我們很抱歉,您無法復原您的密碼,因為您的站臺管理員並未設定 PeerTube 電子郵件系統。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="3188014010833256853" datatype="html">
         <source>Enter your email address and we will send you a link to reset your password.</source>
         <target state="translated">輸入您的電子郵件地址,然後我們將會寄送連結給您重設您的密碼。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group></trans-unit>
       <trans-unit id="1190256911880544559" datatype="html">
         <source>An email with the reset password instructions will be sent to <x id="PH" equiv-text="this.forgotPasswordEmail"/>.
 The link will expire within 1 hour.</source>
@@ -1097,26 +1115,26 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4768749765465246664">
         <source>Email</source>
         <target>電子郵件</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">107</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">105</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">112</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html</context><context context-type="linenumber">4</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">45</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">47</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">8</context></context-group></trans-unit>
       <trans-unit id="3967269098753656610">
         <source>Email address</source>
         <target>電子信箱</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">109</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">113</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html</context><context context-type="linenumber">10</context></context-group></trans-unit>
       <trans-unit id="7808756054397155068" datatype="html">
         <source>Reset</source>
         <target state="translated">重設</target>
         <note priority="1" from="description">Password reset button</note>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">122</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">126</context></context-group></trans-unit>
       <trans-unit id="4319634264526091601" datatype="html">
         <source>on this instance</source>
         <target state="translated">在此站臺</target>
@@ -1442,9 +1460,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2308975396733519902">
         <source>Create an account</source>
         <target>建立帳號</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">50</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.html</context><context context-type="linenumber">54</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.html</context><context context-type="linenumber">100</context></context-group></trans-unit>
       <trans-unit id="3058024914967508975" datatype="html">
         <source>My videos</source>
         <target state="translated">我的影片</target>
@@ -1511,10 +1529,10 @@ The link will expire within 1 hour.</source>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">影片</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">82</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html</context><context context-type="linenumber">215</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
       <trans-unit id="667372110624203230" datatype="html">
         <source>Import jobs concurrency</source>
         <target state="translated">匯入工作並行</target>
@@ -1593,8 +1611,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4424964105331349857" datatype="html">
         <source>I'm a teapot</source>
         <target state="translated">我是茶壺</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">26</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+page-not-found/page-not-found.component.ts</context><context context-type="linenumber">27</context></context-group></trans-unit>
       <trans-unit id="1597262876035959248" datatype="html">
         <source>That's an error.</source>
         <target state="translated">發生錯誤。</target>
@@ -1687,8 +1705,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2971365540217107489" datatype="html">
         <source>Media is too large for the server. Please contact you administrator if you want to increase the limit size.</source>
         <target state="translated">媒體對此伺服器來說太大。如果您想要增加限制大小的話,請聯絡您的管理員。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">62</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">61</context></context-group></trans-unit>
       <trans-unit id="5131854469652959713" datatype="html">
         <source>GLOBAL SEARCH</source>
         <target state="translated">全域搜尋</target>
@@ -2370,13 +2388,13 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9172233176401579786">
         <source>Scheduled</source>
         <target>排定</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-edit.component.ts</context><context context-type="linenumber">192</context></context-group></trans-unit>
+        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-edit.component.ts</context><context context-type="linenumber">192</context></context-group>
+      </trans-unit>
       <trans-unit id="1435317307066082710" datatype="html">
         <source>Hide the video until a specific date</source>
         <target state="translated">在特定日期前隱藏影片</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-edit.component.ts</context><context context-type="linenumber">193</context></context-group></trans-unit>
+        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/shared/video-edit.component.ts</context><context context-type="linenumber">193</context></context-group>
+      </trans-unit>
       <trans-unit id="6148369758871787018">
         <source>Video background image</source>
         <target>影片背景圖片</target>
@@ -2428,8 +2446,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="6161604372916832458" datatype="html">
         <source>Upload on hold</source>
         <target state="translated">暫緩上傳</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">124</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">123</context></context-group></trans-unit>
       <trans-unit id="285180972645018518" datatype="html">
         <source>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</source>
         <target state="translated">抱歉,您的帳號已停用上傳功能。如果您想要新增影片,管理員必須解鎖您的配額。</target>
@@ -3114,11 +3132,7 @@ The link will expire within 1 hour.</source>
         <target>ID</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/system/jobs/jobs.component.html</context><context context-type="linenumber">45</context></context-group>
       </trans-unit>
-      <trans-unit id="2265605798180116441">
-        <source>Follower handle</source>
-        <target>追蹤者處理</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">24</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="5911214550882917183">
         <source>State</source>
         <target>狀態</target>
@@ -3186,11 +3200,7 @@ The link will expire within 1 hour.</source>
         </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/batch-domains-modal.component.html</context><context context-type="linenumber">3</context></context-group>
       </trans-unit>
-      <trans-unit id="6641024648411549335">
-        <source>Host</source>
-        <target>主機</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">31</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="6571718060636962350" datatype="html">
         <source>Redundancy allowed <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></source>
         <target state="translated">允許冗餘 <x id="START_TAG_P_SORTICON"/><x id="CLOSE_TAG_P_SORTICON"/></target>
@@ -3199,9 +3209,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9160510009013134726" datatype="html">
         <source>Unfollow</source>
         <target state="translated">取消追蹤</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">58</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">41</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">48</context></context-group></trans-unit>
       <trans-unit id="8246779176913476983" datatype="html">
         <source>Open instance in a new tab</source>
         <target state="translated">在新分頁中開啟站臺</target>
@@ -3212,28 +3222,20 @@ The link will expire within 1 hour.</source>
       <trans-unit id="9132918641931433659" datatype="html">
         <source>No host found matching current filters.</source>
         <target state="translated">沒有主機符合目前的過濾器。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">70</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7274241885665071790" datatype="html">
         <source>Your instance is not following anyone.</source>
         <target state="translated">您的站臺並未追蹤任何人。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">71</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="4774348799569692380" datatype="html">
         <source>Showing <x id="INTERPOLATION"/> to <x id="INTERPOLATION_1"/> of <x id="INTERPOLATION_2"/> hosts</source>
         <target state="translated">正在顯示 <x id="INTERPOLATION"/> 到 <x id="INTERPOLATION_1"/>,總共有 <x id="INTERPOLATION_2"/> 個主機</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">11</context></context-group>
       </trans-unit>
-      <trans-unit id="6275803119759621687" datatype="html">
-        <source>Follow domains</source>
-        <target state="translated">追蹤網域</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">78</context></context-group>
-      </trans-unit>
-      <trans-unit id="1268699198448750610" datatype="html">
-        <source>Follow instances</source>
-        <target state="translated">追蹤站台</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="9216117865911519658" datatype="html">
         <source>Action</source>
         <target state="translated">動作</target>
@@ -3262,9 +3264,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="8286337167859377104">
         <source>Create user</source>
         <target>建立使用者</target>
-        
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-create.component.ts</context><context context-type="linenumber">93</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.html</context><context context-type="linenumber">20</context></context-group></trans-unit>
+        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-create.component.ts</context><context context-type="linenumber">93</context></context-group>
+        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.html</context><context context-type="linenumber">20</context></context-group>
+      </trans-unit>
       <trans-unit id="8363291180171434623" datatype="html">
         <source>Table parameters</source>
         <target state="translated">參數表</target>
@@ -3283,11 +3285,11 @@ The link will expire within 1 hour.</source>
       <trans-unit id="5248717555542428023">
         <source>Username</source>
         <target>使用者名稱</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group>
-      </trans-unit>
+        
+        
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">83</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">111</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html</context><context context-type="linenumber">6</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+signup/+register/register-step-user.component.html</context><context context-type="linenumber">23</context></context-group></trans-unit>
       <trans-unit id="5428411040014095392" datatype="html">
         <source>e.g. jane_doe</source>
         <target state="translated">例如:jane_doe</target>
@@ -3315,9 +3317,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4145496584631696119">
         <source>Role</source>
         <target>角色</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">136</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">114</context></context-group></trans-unit>
       <trans-unit id="7046347992315328430" datatype="html">
         <source>Transcoding is enabled. The video quota only takes into account <x id="START_TAG_STRONG"/>original<x id="CLOSE_TAG_STRONG"/> video size. <x id="LINE_BREAK"/> At most, this user could upload ~ <x id="INTERPOLATION"/>. </source>
         <target state="translated">轉換編碼已啟用。影片配額僅考慮<x id="START_TAG_STRONG"/>原始<x id="CLOSE_TAG_STRONG"/>影片大小。<x id="LINE_BREAK"/>此使用者最多只能上傳 ~ <x id="INTERPOLATION"/>。 </target>
@@ -3334,15 +3336,9 @@ The link will expire within 1 hour.</source>
       <trans-unit id="2622255144026150901" datatype="html">
         <source>Auth plugin</source>
         <target state="translated">驗證外掛程式</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context>
-          <context context-type="linenumber">188</context>
-        </context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-edit.component.html</context><context context-type="linenumber">188</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">121</context></context-group></trans-unit>
       <trans-unit id="588099657508661970" datatype="html">
         <source>None (local authentication)</source>
         <target state="translated">無(本機驗證)</target>
@@ -3593,6 +3589,12 @@ The link will expire within 1 hour.</source>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/moderation/video-comment-list/video-comment-list.component.html</context><context context-type="linenumber">65</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-ownership/my-ownership.component.html</context><context context-type="linenumber">18</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-abuse-list/abuse-list-table.component.html</context><context context-type="linenumber">41</context></context-group>
+      </trans-unit><trans-unit id="8390803680962035202" datatype="html">
+        <source>Follower</source><target state="new">Follower</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4691552465058437520" datatype="html">
         <source>Commented video</source>
@@ -3892,8 +3894,8 @@ The link will expire within 1 hour.</source>
       <trans-unit id="4917252294930256268" datatype="html">
         <source>It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.</source>
         <target state="translated">看起來您似乎不在 HTTPS 伺服器上。您的網路伺服器必須啟用 TLS 才能追蹤伺服器。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">81</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context><context context-type="linenumber">28</context></context-group></trans-unit>
       <trans-unit id="4058814854824495833" datatype="html">
         <source>Mute domains</source>
         <target state="translated">靜音網域</target>
@@ -5847,11 +5849,8 @@ color: red;
       <trans-unit id="5512878593724620692" datatype="html">
         <source>CHANNELS</source>
         <target state="translated">頻道</target>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context>
-          <context context-type="linenumber">82</context>
-        </context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">81</context></context-group></trans-unit>
       <trans-unit id="3666829335406793239">
         <source>This account does not have channels.</source>
         <target>此帳號沒有頻道。</target>
@@ -5890,6 +5889,12 @@ It will delete <x id="PH_1" equiv-text="videoChannel.videosCount"/> videos uploa
 channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</source>
         <target state="translated">您真的想要刪除 <x id="PH" equiv-text="videoChannel.displayName"/> 嗎?其將會刪除 <x id="PH_1" equiv-text="videoChannel.videosCount"/> 部上傳至此頻道的影片,且您將無法建立其他同名的頻道 (<x id="PH_2" equiv-text="videoChannel.name"/>)!</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">44</context></context-group>
+      </trans-unit><trans-unit id="4433306639366959484" datatype="html">
+        <source>Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</source><target state="new">Please type the name of the video channel (<x id="PH" equiv-text="videoChannel.name"/>) to confirm</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="5387007581996837469" datatype="html">
         <source>My Channels</source>
@@ -6405,13 +6410,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="6979021199788941693">
         <source>Your message has been sent.</source>
         <target>您的訊息已被傳送。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">89</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">88</context></context-group></trans-unit>
       <trans-unit id="2072135752262464360">
         <source>You already sent this form recently</source>
         <target>您最近已發送此表單</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">95</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+about/about-instance/contact-admin-modal.component.ts</context><context context-type="linenumber">94</context></context-group></trans-unit>
       <trans-unit id="819067926858619041" datatype="html">
         <source>Account videos</source>
         <target state="translated">帳號影片</target>
@@ -6456,13 +6461,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">
           <x id="PH"/> 直接帳號追蹤者
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">155</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">154</context></context-group></trans-unit>
       <trans-unit id="6250999352462648289" datatype="html">
         <source>Report this account</source>
         <target state="translated">回報此帳號</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">196</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">195</context></context-group></trans-unit>
       <trans-unit id="1504521795586863905" datatype="html">
         <source>VIDEOS</source>
         <target state="translated">影片</target>
@@ -6472,31 +6477,21 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="25349740244798533">
         <source>Username copied</source>
         <target>使用者名稱已複製</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">121</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">120</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">103</context></context-group></trans-unit>
       <trans-unit id="9221735175659318025" datatype="html">
         <source>1 subscriber</source>
         <target state="translated">1 個訂閱者</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">125</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">124</context></context-group></trans-unit>
       <trans-unit id="4097331874769079975" datatype="html">
         <source><x id="PH"/> subscribers</source>
         <target state="translated"><x id="PH"/> 個訂閱者</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">127</context></context-group>
-      </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">您追蹤的站臺</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">追蹤您的站臺</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context><context context-type="linenumber">3</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+accounts/accounts.component.ts</context><context context-type="linenumber">126</context></context-group></trans-unit>
+      
+      
       <trans-unit id="1035838766454786107" datatype="html">
         <source>Audio-only</source>
         <target state="translated">僅音訊</target>
@@ -6546,6 +6541,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Auto (via ffmpeg)</source>
         <target>自動(透過 ffmpeg)</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/config/shared/config.service.ts</context><context context-type="linenumber">50</context></context-group>
+      </trans-unit><trans-unit id="3642770981085338761" datatype="html">
+        <source>Followers of your instance</source><target state="new">Followers of your instance</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="931255636742351800" datatype="html">
         <source>No limit</source>
@@ -6694,18 +6695,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="2127446333083057097" datatype="html">
         <source>Domain is required.</source>
         <target state="translated">網域必填。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">56</context></context-group>
-      </trans-unit>
-      <trans-unit id="6780793142903080663" datatype="html">
-        <source>Domains entered are invalid.</source>
-        <target state="translated">輸入的域名無效。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
-      <trans-unit id="5886492514458202177" datatype="html">
-        <source>Domains entered contain duplicates.</source>
-        <target state="translated">輸入的域名包含重覆的項目。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">58</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">92</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">101</context></context-group></trans-unit><trans-unit id="7951488350851416577" datatype="html">
+        <source>Hosts entered are invalid.</source><target state="new">Hosts entered are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="1469559036084108672" datatype="html">
+        <source>Hosts entered contain duplicates.</source><target state="new">Hosts entered contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">94</context>
+        </context-group>
+      </trans-unit><trans-unit id="5991533283446904296" datatype="html">
+        <source>Hosts or handles are invalid.</source><target state="new">Hosts or handles are invalid.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">102</context>
+        </context-group>
+      </trans-unit><trans-unit id="6759198394434886237" datatype="html">
+        <source>Hosts or handles contain duplicates.</source><target state="new">Hosts or handles contain duplicates.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context>
+          <context context-type="linenumber">103</context>
+        </context-group>
       </trans-unit>
+      
+      
       <trans-unit id="240806681889331244">
         <source>Unlimited</source>
         <target>無限制</target>
@@ -6869,24 +6886,50 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source><x id="PH"/> removed from instance followers </source>
         <target><x id="PH"/> 已從站臺追蹤者中移除 </target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/followers-list/followers-list.component.ts</context><context context-type="linenumber">81</context></context-group>
+      </trans-unit><trans-unit id="6018246591673612412" datatype="html">
+        <source>Follow</source><target state="new">Follow</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">18</context>
+        </context-group>
+      </trans-unit><trans-unit id="3596798855644241001" datatype="html">
+        <source>1 host (without "http://"), account handle or channel handle per line</source><target state="new">1 host (without "http://"), account handle or channel handle per line</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="2740793005745065895">
         <source><x id="PH"/> is not valid </source>
         <target>
           <x id="PH"/> 無效
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/batch-domains-validators.ts</context><context context-type="linenumber">19</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">27</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/shared/form-validators/host-validators.ts</context><context context-type="linenumber">50</context></context-group></trans-unit>
       <trans-unit id="2355066641781598196">
         <source>Follow request(s) sent!</source>
         <target>追蹤請求已傳送!</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/follow-modal.component.ts</context><context context-type="linenumber">62</context></context-group></trans-unit><trans-unit id="3459358413436264734" datatype="html">
+        <source>Your instance subscriptions</source><target state="new">Your instance subscriptions</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="4245720728052819482">
         <source>Do you really want to unfollow <x id="PH"/>?</source>
         <target>您想要取消追蹤 <x id="PH"/> 嗎?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">57</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">47</context></context-group></trans-unit>
       <trans-unit id="9160510009013134726">
         <source>Unfollow</source>
         <target>取消追蹤</target>
@@ -6895,8 +6938,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3935234189109112926">
         <source>You are not following <x id="PH"/> anymore.</source>
         <target>您無法再追蹤 <x id="PH"/>。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/following-list/following-list.component.ts</context><context context-type="linenumber">54</context></context-group></trans-unit>
       <trans-unit id="2593763089859685916">
         <source>enabled</source>
         <target>已啟用</target>
@@ -7368,9 +7411,9 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1519954996184640001">
         <source>Error</source>
         <target>錯誤</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">104</context></context-group>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group>
-      </trans-unit>
+        
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">103</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/core/notification/notifier.service.ts</context><context context-type="linenumber">18</context></context-group></trans-unit>
       <trans-unit id="5076187961693950167" datatype="html">
         <source>Standard logs</source>
         <target state="translated">標準日誌</target>
@@ -7384,8 +7427,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1886888801485703107">
         <source>User <x id="PH"/> created.</source>
         <target>使用者 <x id="PH"/> 已建立。</target>
-        
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-create.component.ts</context><context context-type="linenumber">76</context></context-group></trans-unit>
+        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-create.component.ts</context><context context-type="linenumber">76</context></context-group>
+      </trans-unit>
       <trans-unit id="8286337167859377104" datatype="html">
         <source>Create user</source>
         <target state="translated">建立使用者</target>
@@ -7411,16 +7454,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>更新使用者密碼</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-edit/user-password.component.ts</context><context context-type="linenumber">52</context></context-group>
       </trans-unit>
-      <trans-unit id="177544274549739411" datatype="html">
-        <source>Following list</source>
-        <target state="translated">追蹤清單</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">28</context></context-group>
-      </trans-unit>
-      <trans-unit id="8092429110007204784" datatype="html">
-        <source>Followers list</source>
-        <target state="translated">追蹤者清單</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/follows/follows.routes.ts</context><context context-type="linenumber">37</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="780323526182667308" datatype="html">
         <source>User <x id="PH"/> updated.</source>
         <target state="translated">使用者 <x id="PH"/> 已更新。</target>
@@ -7456,16 +7491,8 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">聯盟</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">26</context></context-group>
       </trans-unit>
-      <trans-unit id="4682675125751819107" datatype="html">
-        <source>Instances you follow</source>
-        <target state="translated">您追蹤的站臺</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">29</context></context-group>
-      </trans-unit>
-      <trans-unit id="8899833753704589712" datatype="html">
-        <source>Instances following you</source>
-        <target state="translated">追蹤您的站臺</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/admin.component.ts</context><context context-type="linenumber">34</context></context-group>
-      </trans-unit>
+      
+      
       <trans-unit id="3767259920053407667" datatype="html">
         <source>Videos will be deleted, comments will be tombstoned.</source>
         <target state="translated">影片與留言都將會被刪除。</target>
@@ -7496,6 +7523,24 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>設定電子郵件為已驗證</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context><context context-type="linenumber">100</context></context-group>
         <context-group purpose="location"><context context-type="sourcefile">src/app/shared/shared-moderation/user-moderation-dropdown.component.ts</context><context context-type="linenumber">281</context></context-group>
+      </trans-unit><trans-unit id="4207916966377787111" datatype="html">
+        <source>Created</source><target state="new">Created</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">115</context>
+        </context-group>
+      </trans-unit><trans-unit id="8140268298586972139" datatype="html">
+        <source>Daily quota</source><target state="new">Daily quota</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">120</context>
+        </context-group>
+      </trans-unit><trans-unit id="7910076708497708162" datatype="html">
+        <source>Last login</source><target state="new">Last login</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+admin/users/user-list/user-list.component.ts</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3403978719736970622">
         <source>You cannot ban root.</source>
@@ -7800,13 +7845,13 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="1137937154872046253">
         <source>Video channel <x id="PH"/> created.</source>
         <target>影片頻道 <x id="PH"/> 已更新。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="8723777130353305761">
         <source>This name already exists on this instance.</source>
         <target>此名稱已存在於此站臺上。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">73</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts</context><context context-type="linenumber">72</context></context-group></trans-unit>
       <trans-unit id="7589345916094713536">
         <source>Video channel <x id="PH"/> updated.</source>
         <target>影片頻道 <x id="PH"/> 已更新。</target>
@@ -7827,11 +7872,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target state="translated">橫幅已刪除。</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts</context><context context-type="linenumber">152</context></context-group>
       </trans-unit>
-      <trans-unit id="2575302837003821736">
-        <source>Please type the display name of the video channel (<x id="PH"/>) to confirm</source>
-        <target>請輸入影片頻道的顯示名稱 ( <x id="PH"/>) 以確認</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/+my-video-channels/my-video-channels.component.ts</context><context context-type="linenumber">48</context></context-group>
-      </trans-unit>
+      
       <trans-unit id="624066830180032195">
         <source>Video channel <x id="PH"/> deleted.</source>
         <target>影片頻道 <x id="PH"/> 已刪除。</target>
@@ -7981,6 +8022,12 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <source>Ownership change request sent.</source>
         <target>所有權變更請求已發送。</target>
         <context-group purpose="location"><context context-type="sourcefile">src/app/+my-library/my-videos/modals/video-change-ownership.component.ts</context><context context-type="linenumber">64</context></context-group>
+      </trans-unit><trans-unit id="7699622144571229146" datatype="html">
+        <source>Sort by</source><target state="new">Sort by</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/+my-library/my-videos/my-videos.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="3245220240937722814">
         <source>My channels</source>
@@ -8079,7 +8126,7 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
         <target>訂閱帳號</target>
         
         
-      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">704</context></context-group></trans-unit>
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+video-channels/video-channels.component.ts</context><context context-type="linenumber">71</context></context-group><context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">711</context></context-group></trans-unit>
       <trans-unit id="3131904093925601441" datatype="html">
         <source>PLAYLISTS</source>
         <target state="translated">播放清單</target>
@@ -8126,34 +8173,34 @@ channel with the same name (<x id="PH_2" equiv-text="videoChannel.name"/>)!</sou
       <trans-unit id="3779524668013120370">
         <source>Go to my subscriptions</source>
         <target>前往我的訂閱</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">64</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">63</context></context-group></trans-unit>
       <trans-unit id="1136469849928650779">
         <source>Go to my videos</source>
         <target>前往我的影片</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">68</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">67</context></context-group></trans-unit>
       <trans-unit id="7836683738999600376">
         <source>Go to my imports</source>
         <target>前往我的匯入</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="7511292153332773503">
         <source>Go to my channels</source>
         <target>前往我的頻道</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">76</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">75</context></context-group></trans-unit>
       <trans-unit id="2013324644839511073" datatype="html">
         <source>Cannot retrieve OAuth Client credentials: <x id="PH" equiv-text="error.text"/>.
 Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.</source>
         <target state="translated">無法擷取 OAuth 客戶端憑證:<x id="PH" equiv-text="error.text"/>。請確保您已正確設定 PeerTube(config/ 目錄),特別是 "webserver" 部份。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">99</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">98</context></context-group></trans-unit>
       <trans-unit id="375263728166936544">
         <source>You need to reconnect.</source>
         <target>您需要重新連線。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">220</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/auth/auth.service.ts</context><context context-type="linenumber">219</context></context-group></trans-unit>
       <trans-unit id="2206638022166154361">
         <source>Keyboard Shortcuts:</source>
         <target>鍵盤快捷鍵:</target>
@@ -8166,6 +8213,12 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
           <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
           <context context-type="linenumber">98</context>
         </context-group>
+      </trans-unit><trans-unit id="4024404994702813072" datatype="html">
+        <source>In my library</source><target state="new">In my library</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/core/menu/menu.service.ts</context>
+          <context context-type="linenumber">104</context>
+        </context-group>
       </trans-unit>
       <trans-unit id="232050922346936574" datatype="html">
         <source>Trending</source>
@@ -8194,38 +8247,38 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="1266887509445371246">
         <source>Incorrect username or password.</source>
         <target>不正確的使用者名稱或密碼。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">159</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">163</context></context-group></trans-unit>
       <trans-unit id="6974874606619467663" datatype="html">
         <source>Your account is blocked.</source>
         <target state="translated">您的帳號已被封鎖。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">160</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+login/login.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="7939914198003891823" datatype="html">
         <source>any language</source>
         <target state="translated">任何語言</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">263</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">266</context></context-group></trans-unit>
       <trans-unit id="5633144232269377096" datatype="html">
         <source>hide</source>
         <target state="translated">隱藏</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">298</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">301</context></context-group></trans-unit>
       <trans-unit id="8603861867909474404" datatype="html">
         <source>blur</source>
         <target state="translated">模糊</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">302</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">305</context></context-group></trans-unit>
       <trans-unit id="4534458451100881847" datatype="html">
         <source>display</source>
         <target state="translated">顯示</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">306</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">309</context></context-group></trans-unit>
       <trans-unit id="4467323362722952678" datatype="html">
         <source>Unknown</source>
         <target state="translated">未知</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">193</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/menu/menu.component.ts</context><context context-type="linenumber">196</context></context-group></trans-unit>
       <trans-unit id="8781423666414310853">
         <source>Your password has been successfully reset!</source>
         <target>您的密碼已成功重設!</target>
@@ -9803,18 +9856,18 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="968295009933361070">
         <source>Too many attempts, please try again after <x id="PH"/> minutes.</source>
         <target>太多次嘗試,請在 <x id="PH"/> 分鐘後再試。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">67</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">66</context></context-group></trans-unit>
       <trans-unit id="4965472196059235310">
         <source>Too many attempts, please try again later.</source>
         <target>太多次嘗試,請稍後再試。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">69</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">68</context></context-group></trans-unit>
       <trans-unit id="1693549688987384699">
         <source>Server error. Please retry later.</source>
         <target>伺服器錯誤。請稍後重試。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">72</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/core/rest/rest-extractor.service.ts</context><context context-type="linenumber">71</context></context-group></trans-unit>
       <trans-unit id="5927402622550505067" datatype="html">
         <source>Subscribed to all current channels of <x id="PH"/>. You will be notified of all their new videos.</source>
         <target state="translated">訂閱 <x id="PH"/> 目前的所有頻道。您將會收到它們所有的新影片。</target>
@@ -10393,35 +10446,35 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3284171506518522275">
         <source>Your video was uploaded to your account and is private.</source>
         <target>您的影片已上傳到您的帳號並為私人影片。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">161</context></context-group></trans-unit>
       <trans-unit id="5699822024600815733">
         <source>But associated data (tags, description...) will be lost, are you sure you want to leave this page?</source>
         <target>但相關資料(標籤、描述等)將會遺失,您確定您想要離開此頁面嗎?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">163</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">162</context></context-group></trans-unit>
       <trans-unit id="1219739004043110649">
         <source>Your video is not uploaded yet, are you sure you want to leave this page?</source>
         <target>您的影片尚未上傳,您確定您想要離開此頁面嗎?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">165</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">164</context></context-group></trans-unit>
       <trans-unit id="6932865105766151309" datatype="html">
         <source>Upload</source>
         <target state="translated">上傳</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">222</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">221</context></context-group></trans-unit>
       <trans-unit id="8278735427925094503">
         <source>Upload <x id="PH"/> </source>
         <target>上傳 
           <x id="PH"/>
         </target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">224</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">223</context></context-group></trans-unit>
       <trans-unit id="5981816353437801748">
         <source>Video published.</source>
         <target>影片已發佈。</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">245</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-edit/video-add-components/video-upload.component.ts</context><context context-type="linenumber">244</context></context-group></trans-unit>
       <trans-unit id="764164089183618119">
         <source>You have unsaved changes! If you leave, your changes will be lost.</source>
         <target>您有未儲存的變更!如果您離開,您的變更將會遺失。</target>
@@ -10468,28 +10521,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="961774488937452220" datatype="html">
         <source>This video is not available on this instance. Do you want to be redirected on the origin instance: &lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a>?</source>
         <target state="translated">此影片在此站臺上不可用。您想要重新導向至原始站臺:&lt;a href="<x id="PH"/>"><x id="PH_1"/>&lt;/a> 嗎?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">288</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">295</context></context-group></trans-unit>
       <trans-unit id="5761611056224181752" datatype="html">
         <source>Redirection</source>
         <target state="translated">重新導向</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">289</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">296</context></context-group></trans-unit>
       <trans-unit id="8858527736400081688">
         <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
         <target>這部影片包含成人或裸露內容。您確定您想要觀看嗎?</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">335</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">342</context></context-group></trans-unit>
       <trans-unit id="3937119019020041049">
         <source>Mature or explicit content</source>
         <target>成人或裸露內容</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">336</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">343</context></context-group></trans-unit>
       <trans-unit id="1755474755114288376" datatype="html">
         <source>Up Next</source>
         <target state="translated">往下</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">407</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">414</context></context-group></trans-unit>
       <trans-unit id="2159130950882492111" datatype="html">
         <source>Cancel</source>
         <target state="translated">取消</target>
@@ -10498,63 +10551,63 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
       <trans-unit id="3354816756665089864" datatype="html">
         <source>Autoplay is suspended</source>
         <target state="translated">自動播放已暫停</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">409</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">416</context></context-group></trans-unit>
       <trans-unit id="7895294730547405228" datatype="html">
         <source>Enter/exit fullscreen (requires player focus)</source>
         <target state="translated">進入/離開全螢幕(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">678</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group></trans-unit>
       <trans-unit id="7618388257165864759" datatype="html">
         <source>Play/Pause the video (requires player focus)</source>
         <target state="translated">播放/暫停影片(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">679</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">686</context></context-group></trans-unit>
       <trans-unit id="7761890399634216630" datatype="html">
         <source>Mute/unmute the video (requires player focus)</source>
         <target state="translated">靜音/解除靜音影片(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">680</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group></trans-unit>
       <trans-unit id="5996585232248234904" datatype="html">
         <source>Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)</source>
         <target state="translated">跳到影片的百分比:0 是 0%,9 是 90%(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">682</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">689</context></context-group></trans-unit>
       <trans-unit id="3748765405903319998" datatype="html">
         <source>Increase the volume (requires player focus)</source>
         <target state="translated">增加音量(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">684</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group></trans-unit>
       <trans-unit id="5810704036407159982" datatype="html">
         <source>Decrease the volume (requires player focus)</source>
         <target state="translated">降低音量(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">685</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">692</context></context-group></trans-unit>
       <trans-unit id="2622048822548065691" datatype="html">
         <source>Seek the video forward (requires player focus)</source>
         <target state="translated">快轉影片(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">687</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">694</context></context-group></trans-unit>
       <trans-unit id="6540078205109221153" datatype="html">
         <source>Seek the video backward (requires player focus)</source>
         <target state="translated">向後快轉影片(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">688</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">695</context></context-group></trans-unit>
       <trans-unit id="1956491957766210808" datatype="html">
         <source>Increase playback rate (requires player focus)</source>
         <target state="translated">提高播放速度(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">690</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">697</context></context-group></trans-unit>
       <trans-unit id="5495529997674803186" datatype="html">
         <source>Decrease playback rate (requires player focus)</source>
         <target state="translated">減慢播放速度(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">691</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">698</context></context-group></trans-unit>
       <trans-unit id="3178343147230721210" datatype="html">
         <source>Navigate in the video frame by frame (requires player focus)</source>
         <target state="translated">逐畫格瀏覽影片(需要播放器焦點)</target>
-        <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">693</context></context-group>
-      </trans-unit>
+        
+      <context-group purpose="location"><context context-type="sourcefile">src/app/+videos/+video-watch/video-watch.component.ts</context><context context-type="linenumber">700</context></context-group></trans-unit>
       <trans-unit id="8025996572234182184">
         <source>Like the video</source>
         <target>喜歡此影片</target>
index 30d487b111966364280b8afd5c1bd7151f8117c1..bd834db701f03084742b7e6830657378b7f93d51 100644 (file)
@@ -123,12 +123,16 @@ code {
   vertical-align: middle;
 }
 
-.form-error {
+.form-error,
+.form-warning {
   display: block;
-  color: $red;
   margin-top: 5px;
 }
 
+.form-error {
+  color: $red;
+}
+
 .input-error,
 my-input-toggle-hidden ::ng-deep input {
   border-color: $red !important;
index e59d8b9407a1dca5c709f566ca528af63acf8af9..334f386b6b53fd6078eca27c7e7635c591c81ada 100644 (file)
@@ -1,9 +1,9 @@
 import './embed.scss'
 import videojs from 'video.js'
 import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   HTMLServerConfig,
+  HttpStatusCode,
   OAuth2ErrorCode,
   ResultList,
   UserRefreshToken,
@@ -536,6 +536,7 @@ export class PeerTubeEmbed {
         videoCaptions,
         inactivityTimeout: 2500,
         videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views',
+        videoShortUUID: videoInfo.shortUUID,
         videoUUID: videoInfo.uuid,
 
         isLive: videoInfo.isLive,
index 7611ac9aba12253ae0b4e3871c374e970ecb270a..9ce1e1b0ea9204c4d8aa88f36931f7e1f4fd85d8 100644 (file)
@@ -74,7 +74,6 @@
   },
   "dependencies": {
     "@uploadx/core": "^4.4.0",
-    "apicache": "1.6.2",
     "async": "^3.0.1",
     "async-lru": "^1.1.1",
     "bcrypt": "5.0.1",
   },
   "devDependencies": {
     "@openapitools/openapi-generator-cli": "^2.1.4",
-    "@types/apicache": "^1.2.0",
     "@types/async": "^3.0.0",
     "@types/async-lock": "^1.1.0",
     "@types/bcrypt": "^5.0.0",
     "@types/chai": "^4.0.4",
     "@types/chai-json-schema": "^1.4.3",
     "@types/chai-xml": "^0.3.1",
-    "@types/config": "^0.0.38",
+    "@types/config": "^0.0.39",
     "@types/express": "4.17.9",
     "@types/express-rate-limit": "^5.0.0",
     "@types/fluent-ffmpeg": "^2.1.16",
     "@types/lodash": "^4.14.64",
     "@types/lru-cache": "^5.1.0",
     "@types/magnet-uri": "^5.1.1",
-    "@types/maildev": "^0.0.2",
+    "@types/maildev": "^0.0.3",
     "@types/memoizee": "^0.4.2",
     "@types/mkdirp": "^1.0.0",
     "@types/mocha": "^8.0.3",
     "source-map-support": "^0.5.0",
     "supertest": "^6.0.1",
     "swagger-cli": "^4.0.2",
-    "ts-node": "10.0.0",
+    "ts-node": "10.1.0",
     "typescript": "^4.0.5"
   },
   "bundlewatch": {
index 0cadb36d94a5897630783ac8b71bd68905491bdf..5dcf9b01b51472d37f04d2d47e003a19126c7fdc 100644 (file)
@@ -2,21 +2,11 @@ import { registerTSPaths } from '../server/helpers/register-ts-paths'
 registerTSPaths()
 
 import * as autocannon from 'autocannon'
-import {
-  addVideoCommentReply,
-  addVideoCommentThread,
-  createVideoCaption,
-  flushAndRunServer,
-  getVideosList,
-  killallServers,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo
-} from '@shared/extra-utils'
-import { Video, VideoPrivacy } from '@shared/models'
 import { writeJson } from 'fs-extra'
+import { createSingleServer, killallServers, PeerTubeServer, setAccessTokensToServers } from '@shared/extra-utils'
+import { Video, VideoPrivacy } from '@shared/models'
 
-let server: ServerInfo
+let server: PeerTubeServer
 let video: Video
 let threadId: number
 
@@ -25,7 +15,7 @@ const outfile = process.argv[2]
 run()
   .catch(err => console.error(err))
   .finally(() => {
-    if (server) killallServers([ server ])
+    if (server) return killallServers([ server ])
   })
 
 function buildAuthorizationHeader () {
@@ -198,7 +188,7 @@ function runBenchmark (options: {
 }
 
 async function prepare () {
-  server = await flushAndRunServer(1, {
+  server = await createSingleServer(1, {
     rates_limit: {
       api: {
         max: 5_000_000
@@ -207,7 +197,7 @@ async function prepare () {
   })
   await setAccessTokensToServers([ server ])
 
-  const videoAttributes = {
+  const attributes = {
     name: 'my super video',
     category: 2,
     nsfw: true,
@@ -220,33 +210,29 @@ async function prepare () {
   }
 
   for (let i = 0; i < 10; i++) {
-    Object.assign(videoAttributes, { name: 'my super video ' + i })
-    await uploadVideo(server.url, server.accessToken, videoAttributes)
+    await server.videos.upload({ attributes: { ...attributes, name: 'my super video ' + i } })
   }
 
-  const resVideos = await getVideosList(server.url)
-  video = resVideos.body.data.find(v => v.name === 'my super video 1')
+  const { data } = await server.videos.list()
+  video = data.find(v => v.name === 'my super video 1')
 
   for (let i = 0; i < 10; i++) {
     const text = 'my super first comment'
-    const res = await addVideoCommentThread(server.url, server.accessToken, video.id, text)
-    threadId = res.body.comment.id
+    const created = await server.comments.createThread({ videoId: video.id, text })
+    threadId = created.id
 
     const text1 = 'my super answer to thread 1'
-    const childCommentRes = await addVideoCommentReply(server.url, server.accessToken, video.id, threadId, text1)
-    const childCommentId = childCommentRes.body.comment.id
+    const child = await server.comments.addReply({ videoId: video.id, toCommentId: threadId, text: text1 })
 
     const text2 = 'my super answer to answer of thread 1'
-    await addVideoCommentReply(server.url, server.accessToken, video.id, childCommentId, text2)
+    await server.comments.addReply({ videoId: video.id, toCommentId: child.id, text: text2 })
 
     const text3 = 'my second answer to thread 1'
-    await addVideoCommentReply(server.url, server.accessToken, video.id, threadId, text3)
+    await server.comments.addReply({ videoId: video.id, toCommentId: threadId, text: text3 })
   }
 
   for (const caption of [ 'ar', 'fr', 'en', 'zh' ]) {
-    await createVideoCaption({
-      url: server.url,
-      accessToken: server.accessToken,
+    await server.captions.add({
       language: caption,
       videoId: video.id,
       fixture: 'subtitle-good2.vtt'
index 07e37e0eee82ce393f20ac7634e5c03b795d2c9f..71b1be53b78e39db3ee00f4879de3a51d5c94aef 100755 (executable)
@@ -47,11 +47,12 @@ if [ "$1" = "client" ]; then
 
     feedsFiles=$(findTestFiles ./dist/server/tests/feeds)
     helperFiles=$(findTestFiles ./dist/server/tests/helpers)
+    libFiles=$(findTestFiles ./dist/server/tests/lib)
     miscFiles="./dist/server/tests/client.js ./dist/server/tests/misc-endpoints.js"
     # Not in plugin task, it needs an index.html
     pluginFiles="./dist/server/tests/plugins/html-injection.js"
 
-    MOCHA_PARALLEL=true runTest "$1" 2 $feedsFiles $helperFiles $miscFiles $pluginFiles
+    MOCHA_PARALLEL=true runTest "$1" 2 $feedsFiles $helperFiles $miscFiles $pluginFiles $libFiles
 elif [ "$1" = "cli-plugin" ]; then
     npm run build:server
     npm run setup:cli
@@ -76,7 +77,7 @@ elif [ "$1" = "api-2" ]; then
     serverFiles=$(findTestFiles ./dist/server/tests/api/server)
     usersFiles=$(findTestFiles ./dist/server/tests/api/users)
 
-    MOCHA_PARALLEL=true runTest "$1" 3 $serverFiles $usersFiles $liveFiles
+    MOCHA_PARALLEL=true runTest "$1" 3 $liveFiles $serverFiles $usersFiles
 elif [ "$1" = "api-3" ]; then
     npm run build:server
 
index db5af3f9152dadc350d624c6a49f34a914eeb346..935ed3c5a5910bdb22ab6bc952919bc8e1bd147e 100755 (executable)
@@ -1,7 +1,7 @@
 import { registerTSPaths } from '../server/helpers/register-ts-paths'
 registerTSPaths()
 
-import { execCLI } from '@shared/extra-utils'
+import { CLICommand } from '@shared/extra-utils'
 
 run()
   .then(() => process.exit(0))
@@ -59,7 +59,7 @@ async function run () {
 }
 
 async function getGitContributors () {
-  const output = await execCLI(`git --no-pager shortlog -sn < /dev/tty | sed 's/^\\s\\+[0-9]\\+\\s\\+//g'`)
+  const output = await CLICommand.exec(`git --no-pager shortlog -sn < /dev/tty | sed 's/^\\s\\+[0-9]\\+\\s\\+//g'`)
 
   return output.split('\n')
                .filter(l => !!l)
index 9692d76bacd778e8b8e800c57e5a89021b7f784b..bde9d1e016cc2d8ce8d179951ada961a8f9f36ef 100644 (file)
@@ -19,13 +19,13 @@ run()
     process.exit(-1)
   })
 
-let currentVideoId = null
-let currentFile = null
+let currentVideoId: string
+let currentFilePath: string
 
 process.on('SIGINT', async function () {
   console.log('Cleaning up temp files')
-  await remove(`${currentFile}_backup`)
-  await remove(`${dirname(currentFile)}/${currentVideoId}-transcoded.mp4`)
+  await remove(`${currentFilePath}_backup`)
+  await remove(`${dirname(currentFilePath)}/${currentVideoId}-transcoded.mp4`)
   process.exit(0)
 })
 
@@ -40,12 +40,12 @@ async function run () {
     currentVideoId = video.id
 
     for (const file of video.VideoFiles) {
-      currentFile = getVideoFilePath(video, file)
+      currentFilePath = getVideoFilePath(video, file)
 
       const [ videoBitrate, fps, resolution ] = await Promise.all([
-        getVideoFileBitrate(currentFile),
-        getVideoFileFPS(currentFile),
-        getVideoFileResolution(currentFile)
+        getVideoFileBitrate(currentFilePath),
+        getVideoFileFPS(currentFilePath),
+        getVideoFileResolution(currentFilePath)
       ])
 
       const maxBitrate = getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)
@@ -53,25 +53,27 @@ async function run () {
       if (isMaxBitrateExceeded) {
         console.log(
           'Optimizing video file %s with bitrate %s kbps (max: %s kbps)',
-          basename(currentFile), videoBitrate / 1000, maxBitrate / 1000
+          basename(currentFilePath), videoBitrate / 1000, maxBitrate / 1000
         )
 
-        const backupFile = `${currentFile}_backup`
-        await copy(currentFile, backupFile)
+        const backupFile = `${currentFilePath}_backup`
+        await copy(currentFilePath, backupFile)
 
         await optimizeOriginalVideofile(video, file)
+        // Update file path, the video filename changed
+        currentFilePath = getVideoFilePath(video, file)
 
         const originalDuration = await getDurationFromVideoFile(backupFile)
-        const newDuration = await getDurationFromVideoFile(currentFile)
+        const newDuration = await getDurationFromVideoFile(currentFilePath)
 
         if (originalDuration === newDuration) {
-          console.log('Finished optimizing %s', basename(currentFile))
+          console.log('Finished optimizing %s', basename(currentFilePath))
           await remove(backupFile)
           continue
         }
 
-        console.log('Failed to optimize %s, restoring original', basename(currentFile))
-        await move(backupFile, currentFile, { overwrite: true })
+        console.log('Failed to optimize %s, restoring original', basename(currentFilePath))
+        await move(backupFile, currentFilePath, { overwrite: true })
         await createTorrentAndSetInfoHash(video, file)
         await file.save()
       }
index c1650358923010a3594aae5dffb6a9f0264a63e5..6cd3a1860aed75898c357e6c73462a058ce41f02 100755 (executable)
@@ -6,9 +6,8 @@ import { createReadStream, readdir } from 'fs-extra'
 import { join } from 'path'
 import { createInterface } from 'readline'
 import * as winston from 'winston'
-import { labelFormatter } from '../server/helpers/logger'
+import { labelFormatter, mtimeSortFilesDesc } from '../server/helpers/logger'
 import { CONFIG } from '../server/initializers/config'
-import { mtimeSortFilesDesc } from '../shared/core-utils/logs/logs'
 import { inspect } from 'util'
 import { format as sqlFormat } from 'sql-formatter'
 
index 58d24816e345e5e60c966c97999add985c31d63a..5b029d215a703de61bfad4fe6cee4d9894394692 100755 (executable)
@@ -2,11 +2,11 @@ import { registerTSPaths } from '../server/helpers/register-ts-paths'
 registerTSPaths()
 
 import * as prompt from 'prompt'
-import { join } from 'path'
+import { join, basename } from 'path'
 import { CONFIG } from '../server/initializers/config'
 import { VideoModel } from '../server/models/video/video'
 import { initDatabaseModels } from '../server/initializers/database'
-import { readdir, remove } from 'fs-extra'
+import { readdir, remove, stat } from 'fs-extra'
 import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy'
 import * as Bluebird from 'bluebird'
 import { getUUIDFromFilename } from '../server/helpers/utils'
@@ -14,6 +14,7 @@ import { ThumbnailModel } from '../server/models/video/thumbnail'
 import { ActorImageModel } from '../server/models/actor/actor-image'
 import { uniq, values } from 'lodash'
 import { ThumbnailType } from '@shared/models'
+import { VideoFileModel } from '@server/models/video/video-file'
 
 run()
   .then(() => process.exit(0))
@@ -37,8 +38,8 @@ async function run () {
   console.log('Detecting files to remove, it could take a while...')
 
   toDelete = toDelete.concat(
-    await pruneDirectory(CONFIG.STORAGE.VIDEOS_DIR, doesVideoExist(true)),
-    await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesVideoExist(true)),
+    await pruneDirectory(CONFIG.STORAGE.VIDEOS_DIR, doesWebTorrentFileExist()),
+    await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesTorrentFileExist()),
 
     await pruneDirectory(CONFIG.STORAGE.REDUNDANCY_DIR, doesRedundancyExist),
 
@@ -78,26 +79,27 @@ async function pruneDirectory (directory: string, existFun: ExistFun) {
 
   const toDelete: string[] = []
   await Bluebird.map(files, async file => {
-    if (await existFun(file) !== true) {
-      toDelete.push(join(directory, file))
+    const filePath = join(directory, file)
+
+    if (await existFun(filePath) !== true) {
+      toDelete.push(filePath)
     }
   }, { concurrency: 20 })
 
   return toDelete
 }
 
-function doesVideoExist (keepOnlyOwned: boolean) {
-  return async (file: string) => {
-    const uuid = getUUIDFromFilename(file)
-    const video = await VideoModel.load(uuid)
+function doesWebTorrentFileExist () {
+  return (filePath: string) => VideoFileModel.doesOwnedWebTorrentVideoFileExist(basename(filePath))
+}
 
-    return video && (keepOnlyOwned === false || video.isOwned())
-  }
+function doesTorrentFileExist () {
+  return (filePath: string) => VideoFileModel.doesOwnedTorrentFileExist(basename(filePath))
 }
 
 function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) {
-  return async (file: string) => {
-    const thumbnail = await ThumbnailModel.loadByFilename(file, type)
+  return async (filePath: string) => {
+    const thumbnail = await ThumbnailModel.loadByFilename(basename(filePath), type)
     if (!thumbnail) return false
 
     if (keepOnlyOwned) {
@@ -109,21 +111,20 @@ function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) {
   }
 }
 
-async function doesActorImageExist (file: string) {
-  const image = await ActorImageModel.loadByName(file)
+async function doesActorImageExist (filePath: string) {
+  const image = await ActorImageModel.loadByName(basename(filePath))
 
   return !!image
 }
 
-async function doesRedundancyExist (file: string) {
-  const uuid = getUUIDFromFilename(file)
-  const video = await VideoModel.loadWithFiles(uuid)
-
-  if (!video) return false
-
-  const isPlaylist = file.includes('.') === false
+async function doesRedundancyExist (filePath: string) {
+  const isPlaylist = (await stat(filePath)).isDirectory()
 
   if (isPlaylist) {
+    const uuid = getUUIDFromFilename(filePath)
+    const video = await VideoModel.loadWithFiles(uuid)
+    if (!video) return false
+
     const p = video.getHLSPlaylist()
     if (!p) return false
 
@@ -131,19 +132,10 @@ async function doesRedundancyExist (file: string) {
     return !!redundancy
   }
 
-  const resolution = parseInt(file.split('-')[5], 10)
-  if (isNaN(resolution)) {
-    console.error('Cannot prune %s because we cannot guess guess the resolution.', file)
-    return true
-  }
-
-  const videoFile = video.getWebTorrentFile(resolution)
-  if (!videoFile) {
-    console.error('Cannot find webtorrent file of video %s - %d', video.url, resolution)
-    return true
-  }
+  const file = await VideoFileModel.loadByFilename(basename(filePath))
+  if (!file) return false
 
-  const redundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id)
+  const redundancy = await VideoRedundancyModel.loadLocalByFileId(file.id)
   return !!redundancy
 }
 
index 59268422502c654876ec33f262410c9ea5423129..9e8dd41cabc44f88ec5035adc63cbcb0dd4fa003 100755 (executable)
@@ -16,7 +16,6 @@ import { VideoShareModel } from '../server/models/video/video-share'
 import { VideoCommentModel } from '../server/models/video/video-comment'
 import { AccountModel } from '../server/models/account/account'
 import { VideoChannelModel } from '../server/models/video/video-channel'
-import { VideoStreamingPlaylistModel } from '../server/models/video/video-streaming-playlist'
 import { initDatabaseModels } from '../server/initializers/database'
 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 import { getServerActor } from '@server/models/application/application'
@@ -128,13 +127,17 @@ async function run () {
     for (const file of video.VideoFiles) {
       console.log('Updating torrent file %s of video %s.', file.resolution, video.uuid)
       await createTorrentAndSetInfoHash(video, file)
+
+      await file.save()
     }
 
-    for (const playlist of video.VideoStreamingPlaylists) {
-      playlist.playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
-      playlist.segmentsSha256Url = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive)
+    const playlist = video.getHLSPlaylist()
+    for (const file of (playlist?.VideoFiles || [])) {
+      console.log('Updating fragmented torrent file %s of video %s.', file.resolution, video.uuid)
+
+      await createTorrentAndSetInfoHash(video, file)
 
-      await playlist.save()
+      await file.save()
     }
   }
 }
index e46300dcef4be56974a82d84ba58bee6d27af6a9..bfc7ee1450867bfdddf45d54f18ade4ff5f92fd8 100644 (file)
--- a/server.ts
+++ b/server.ts
@@ -125,7 +125,7 @@ import { PeerTubeVersionCheckScheduler } from './server/lib/schedulers/peertube-
 import { Hooks } from './server/lib/plugins/hooks'
 import { PluginManager } from './server/lib/plugins/plugin-manager'
 import { LiveManager } from './server/lib/live'
-import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from './shared/models/http/http-error-codes'
 import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
 import { ServerConfigManager } from '@server/lib/server-config-manager'
 
@@ -305,13 +305,19 @@ async function startApplication () {
   updateStreamingPlaylistsInfohashesIfNeeded()
     .catch(err => logger.error('Cannot update streaming playlist infohashes.', { err }))
 
-  if (cliOptions.plugins) await PluginManager.Instance.registerPluginsAndThemes()
-
   LiveManager.Instance.init()
   if (CONFIG.LIVE.ENABLED) LiveManager.Instance.run()
 
   // Make server listening
-  server.listen(port, hostname, () => {
+  server.listen(port, hostname, async () => {
+    if (cliOptions.plugins) {
+      try {
+        await PluginManager.Instance.registerPluginsAndThemes()
+      } catch (err) {
+        logger.error('Cannot register plugins and themes.', { err })
+      }
+    }
+
     logger.info('HTTP server listening on %s:%d', hostname, port)
     logger.info('Web server: %s', WEBSERVER.URL)
 
index d7de1b9bdba4636ec7ff618e1904241db1c480da..bef4bc068cfe85e2b2f20a6c853549ec08b4ca76 100644 (file)
@@ -24,7 +24,7 @@ import {
   videosCustomGetValidator,
   videosShareValidator
 } from '../../middlewares'
-import { cacheRoute } from '../../middlewares/cache'
+import { cacheRoute } from '../../middlewares/cache/cache'
 import { getAccountVideoRateValidatorFactory, videoCommentGetValidator } from '../../middlewares/validators'
 import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy'
 import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
@@ -77,7 +77,7 @@ activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
 activityPubClientRouter.get(
   [ '/videos/watch/:id', '/w/:id' ],
   executeIfActivityPub,
-  asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
   asyncMiddleware(videosCustomGetValidator('all')),
   asyncMiddleware(videoController)
 )
index 14f301ab726c5ba3959536e68e510102a3c3b9ed..30662990a0e628e9025de95ff4e59eb95fffd374 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { InboxManager } from '@server/lib/activitypub/inbox-manager'
 import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, RootActivity } from '../../../shared'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
 import { logger } from '../../helpers/logger'
 import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChannelValidator, signatureValidator } from '../../middlewares'
index ba5b948405480708a50e7f24b1017e29fdc5ca94..e851365e928924e7ed739fc5824ce3529f739db0 100644 (file)
@@ -6,7 +6,7 @@ import { AbuseModel } from '@server/models/abuse/abuse'
 import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
 import { getServerActor } from '@server/models/application/application'
 import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '@shared/models'
 import { AbuseCreate, AbuseState, UserRight } from '../../../shared'
 import { getFormattedObjects } from '../../helpers/utils'
 import { sequelizeTypescript } from '../../initializers/database'
index 49a8e3195028bdeb6eccffb5c7bbd6b65f649a86..55e2aaf62d6615edeb179fc02e4d3689a6e16016 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
+import { pickCommonVideoQuery } from '@server/helpers/query'
 import { getServerActor } from '@server/models/application/application'
-import { VideosWithSearchCommonQuery } from '@shared/models'
 import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
 import { getFormattedObjects } from '../../helpers/utils'
 import { JobQueue } from '../../lib/job-queue'
@@ -159,27 +159,19 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
   const account = res.locals.account
   const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
   const countVideos = getCountVideos(req)
-  const query = req.query as VideosWithSearchCommonQuery
+  const query = pickCommonVideoQuery(req.query)
 
   const apiOptions = await Hooks.wrapObject({
+    ...query,
+
     followerActorId,
-    start: query.start,
-    count: query.count,
-    sort: query.sort,
+    search: req.query.search,
     includeLocalVideos: true,
-    categoryOneOf: query.categoryOneOf,
-    licenceOneOf: query.licenceOneOf,
-    languageOneOf: query.languageOneOf,
-    tagsOneOf: query.tagsOneOf,
-    tagsAllOf: query.tagsAllOf,
-    filter: query.filter,
-    isLive: query.isLive,
     nsfw: buildNSFWFilter(res, query.nsfw),
     withFiles: false,
     accountId: account.id,
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
-    countVideos,
-    search: query.search
+    countVideos
   }, 'filter:api.accounts.videos.list.params')
 
   const resultList = await Hooks.wrapPromiseFun(
index 192daccde80e5e638adcf0d44dea0dbd920e071d..62121ece5a2021e5fa273f99656ac0efedb4a040 100644 (file)
@@ -1,10 +1,10 @@
 import * as express from 'express'
-import { asyncMiddleware, authenticate } from '../../middlewares'
+import { removeComment } from '@server/lib/video-comment'
 import { bulkRemoveCommentsOfValidator } from '@server/middlewares/validators/bulk'
 import { VideoCommentModel } from '@server/models/video/video-comment'
-import { removeComment } from '@server/lib/video-comment'
+import { HttpStatusCode } from '@shared/models'
 import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { asyncMiddleware, authenticate } from '../../middlewares'
 
 const bulkRouter = express.Router()
 
index 9bd8c21c5d0d40ef8d96d2dc6bf81fbcae73de51..ee733a38c87fc139b1a4dea92aaae64427177756 100644 (file)
@@ -1,8 +1,8 @@
-import { ServerConfigManager } from '@server/lib/server-config-manager'
 import * as express from 'express'
 import { remove, writeJSON } from 'fs-extra'
 import { snakeCase } from 'lodash'
 import validator from 'validator'
+import { ServerConfigManager } from '@server/lib/server-config-manager'
 import { UserRight } from '../../../shared'
 import { About } from '../../../shared/models/server/about.model'
 import { CustomConfig } from '../../../shared/models/server/custom-config.model'
index c19f03c560a40238dcea06e3e594cb9f30e5077b..68d8c2ea49b127035c439801080849e5a123c02f 100644 (file)
@@ -1,8 +1,7 @@
 import * as express from 'express'
 import { ServerConfigManager } from '@server/lib/server-config-manager'
 import { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
-import { HttpStatusCode } from '@shared/core-utils'
-import { UserRight } from '@shared/models'
+import { HttpStatusCode, UserRight } from '@shared/models'
 import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
 
 const customPageRouter = express.Router()
index 28378654ab1175165b44de221f62614aaa37141c..93b14dadbacf3c3a244e3531f63818e0d2a65678 100644 (file)
@@ -1,7 +1,7 @@
 import * as cors from 'cors'
 import * as express from 'express'
 import * as RateLimit from 'express-rate-limit'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models'
 import { badRequest } from '../../helpers/express-utils'
 import { CONFIG } from '../../initializers/config'
 import { abuseRouter } from './abuse'
index 15bbf5c4df12a62a8dcd26e06bebe584b55018bf..f95f06864dbef3cb857e71a8ebc6b7818dd1bf4c 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { OAuthClientLocal } from '../../../shared'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { logger } from '../../helpers/logger'
 import { CONFIG } from '../../initializers/config'
 import { asyncMiddleware, openapiOperationDoc } from '../../middlewares'
index ad879aad6c677bd33b80562be861a06afb340dca..2dfac15ef3019497ebf6f5c706db478efbe309fc 100644 (file)
@@ -1,12 +1,13 @@
 import * as express from 'express'
+import * as memoizee from 'memoizee'
+import { logger } from '@server/helpers/logger'
+import { Hooks } from '@server/lib/plugins/hooks'
+import { VideoModel } from '@server/models/video/video'
+import { CategoryOverview, ChannelOverview, TagOverview, VideosOverview } from '../../../shared/models/overviews'
 import { buildNSFWFilter } from '../../helpers/express-utils'
-import { VideoModel } from '../../models/video/video'
+import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants'
 import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares'
 import { TagModel } from '../../models/video/tag'
-import { CategoryOverview, ChannelOverview, TagOverview, VideosOverview } from '../../../shared/models/overviews'
-import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants'
-import * as memoizee from 'memoizee'
-import { logger } from '@server/helpers/logger'
 
 const overviewsRouter = express.Router()
 
@@ -108,7 +109,7 @@ async function getVideos (
   res: express.Response,
   where: { videoChannelId?: number, tagsOneOf?: string[], categoryOneOf?: number[] }
 ) {
-  const query = Object.assign({
+  const query = await Hooks.wrapObject({
     start: 0,
     count: 12,
     sort: '-createdAt',
@@ -116,10 +117,16 @@ async function getVideos (
     nsfw: buildNSFWFilter(res),
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
     withFiles: false,
-    countVideos: false
-  }, where)
+    countVideos: false,
+
+    ...where
+  }, 'filter:api.overviews.videos.list.params')
 
-  const { data } = await VideoModel.listForApi(query)
+  const { data } = await Hooks.wrapPromiseFun(
+    VideoModel.listForApi,
+    query,
+    'filter:api.overviews.videos.list.result'
+  )
 
   return data.map(d => d.toFormattedJSON())
 }
index 1e6a02c496f17503f49dad18ac8da0089a95c953..3a9ef34e8a464cce0ccb85b6b172ab14435d2df6 100644 (file)
@@ -23,8 +23,8 @@ import {
   updatePluginSettingsValidator
 } from '@server/middlewares/validators/plugins'
 import { PluginModel } from '@server/models/server/plugin'
-import { HttpStatusCode } from '@shared/core-utils'
 import {
+  HttpStatusCode,
   InstallOrUpdatePlugin,
   ManagePlugin,
   PeertubePluginIndexList,
index 16beeed60ababcc28112170ffdfae58f831b9a63..eef22250636aaabf7ecc48c4a1d29d15f62c5851 100644 (file)
@@ -1,14 +1,14 @@
 import * as express from 'express'
 import { sanitizeUrl } from '@server/helpers/core-utils'
+import { pickSearchChannelQuery } from '@server/helpers/query'
 import { doJSONRequest } from '@server/helpers/requests'
 import { CONFIG } from '@server/initializers/config'
 import { WEBSERVER } from '@server/initializers/constants'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
 import { getServerActor } from '@server/models/application/application'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { ResultList, VideoChannel } from '@shared/models'
-import { VideoChannelsSearchQuery } from '../../../../shared/models/search'
+import { HttpStatusCode, ResultList, VideoChannel } from '@shared/models'
+import { VideoChannelsSearchQueryAfterSanitize } from '../../../../shared/models/search'
 import { isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils'
 import { logger } from '../../../helpers/logger'
 import { getFormattedObjects } from '../../../helpers/utils'
@@ -46,8 +46,8 @@ export { searchChannelsRouter }
 // ---------------------------------------------------------------------------
 
 function searchVideoChannels (req: express.Request, res: express.Response) {
-  const query: VideoChannelsSearchQuery = req.query
-  const search = query.search
+  const query = pickSearchChannelQuery(req.query)
+  let search = query.search || ''
 
   const parts = search.split('@')
 
@@ -58,7 +58,7 @@ function searchVideoChannels (req: express.Request, res: express.Response) {
   if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res)
 
   // @username -> username to search in DB
-  if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '')
+  if (search.startsWith('@')) search = search.replace(/^@/, '')
 
   if (isSearchIndexSearch(query)) {
     return searchVideoChannelsIndex(query, res)
@@ -67,7 +67,7 @@ function searchVideoChannels (req: express.Request, res: express.Response) {
   return searchVideoChannelsDB(query, res)
 }
 
-async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) {
+async function searchVideoChannelsIndex (query: VideoChannelsSearchQueryAfterSanitize, res: express.Response) {
   const result = await buildMutedForSearchIndex(res)
 
   const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params')
@@ -91,15 +91,13 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e
   }
 }
 
-async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) {
+async function searchVideoChannelsDB (query: VideoChannelsSearchQueryAfterSanitize, res: express.Response) {
   const serverActor = await getServerActor()
 
   const apiOptions = await Hooks.wrapObject({
-    actorId: serverActor.id,
-    search: query.search,
-    start: query.start,
-    count: query.count,
-    sort: query.sort
+    ...query,
+
+    actorId: serverActor.id
   }, 'filter:api.search.video-channels.local.list.params')
 
   const resultList = await Hooks.wrapPromiseFun(
index b231ff1e20830fd77a0ab5d4162ec799c5660910..0a56f19b7c394f567ecbd9b59d5118c7bfb07fac 100644 (file)
@@ -2,17 +2,18 @@ import * as express from 'express'
 import { sanitizeUrl } from '@server/helpers/core-utils'
 import { isUserAbleToSearchRemoteURI } from '@server/helpers/express-utils'
 import { logger } from '@server/helpers/logger'
+import { pickSearchPlaylistQuery } from '@server/helpers/query'
 import { doJSONRequest } from '@server/helpers/requests'
 import { getFormattedObjects } from '@server/helpers/utils'
 import { CONFIG } from '@server/initializers/config'
+import { WEBSERVER } from '@server/initializers/constants'
 import { getOrCreateAPVideoPlaylist } from '@server/lib/activitypub/playlists/get'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
 import { getServerActor } from '@server/models/application/application'
 import { VideoPlaylistModel } from '@server/models/video/video-playlist'
 import { MVideoPlaylistFullSummary } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
-import { ResultList, VideoPlaylist, VideoPlaylistsSearchQuery } from '@shared/models'
+import { HttpStatusCode, ResultList, VideoPlaylist, VideoPlaylistsSearchQueryAfterSanitize } from '@shared/models'
 import {
   asyncMiddleware,
   openapiOperationDoc,
@@ -23,7 +24,6 @@ import {
   videoPlaylistsListSearchValidator,
   videoPlaylistsSearchSortValidator
 } from '../../../middlewares'
-import { WEBSERVER } from '@server/initializers/constants'
 
 const searchPlaylistsRouter = express.Router()
 
@@ -45,7 +45,7 @@ export { searchPlaylistsRouter }
 // ---------------------------------------------------------------------------
 
 function searchVideoPlaylists (req: express.Request, res: express.Response) {
-  const query: VideoPlaylistsSearchQuery = req.query
+  const query = pickSearchPlaylistQuery(req.query)
   const search = query.search
 
   if (isURISearch(search)) return searchVideoPlaylistsURI(search, res)
@@ -57,7 +57,7 @@ function searchVideoPlaylists (req: express.Request, res: express.Response) {
   return searchVideoPlaylistsDB(query, res)
 }
 
-async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQuery, res: express.Response) {
+async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQueryAfterSanitize, res: express.Response) {
   const result = await buildMutedForSearchIndex(res)
 
   const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-playlists.index.list.params')
@@ -81,15 +81,13 @@ async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQuery, res:
   }
 }
 
-async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: express.Response) {
+async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQueryAfterSanitize, res: express.Response) {
   const serverActor = await getServerActor()
 
   const apiOptions = await Hooks.wrapObject({
-    followerActorId: serverActor.id,
-    search: query.search,
-    start: query.start,
-    count: query.count,
-    sort: query.sort
+    ...query,
+
+    followerActorId: serverActor.id
   }, 'filter:api.search.video-playlists.local.list.params')
 
   const resultList = await Hooks.wrapPromiseFun(
index b626baa28421e95c4a3a4126dd1b38da2f835e30..4a6ce0de4e6886f87929686bf44888aa98f16b86 100644 (file)
@@ -1,14 +1,14 @@
 import * as express from 'express'
 import { sanitizeUrl } from '@server/helpers/core-utils'
+import { pickSearchVideoQuery } from '@server/helpers/query'
 import { doJSONRequest } from '@server/helpers/requests'
 import { CONFIG } from '@server/initializers/config'
 import { WEBSERVER } from '@server/initializers/constants'
 import { getOrCreateAPVideo } from '@server/lib/activitypub/videos'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { ResultList, Video } from '@shared/models'
-import { VideosSearchQuery } from '../../../../shared/models/search'
+import { HttpStatusCode, ResultList, Video } from '@shared/models'
+import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search'
 import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils'
 import { logger } from '../../../helpers/logger'
 import { getFormattedObjects } from '../../../helpers/utils'
@@ -47,7 +47,7 @@ export { searchVideosRouter }
 // ---------------------------------------------------------------------------
 
 function searchVideos (req: express.Request, res: express.Response) {
-  const query: VideosSearchQuery = req.query
+  const query = pickSearchVideoQuery(req.query)
   const search = query.search
 
   if (isURISearch(search)) {
@@ -61,10 +61,10 @@ function searchVideos (req: express.Request, res: express.Response) {
   return searchVideosDB(query, res)
 }
 
-async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) {
+async function searchVideosIndex (query: VideosSearchQueryAfterSanitize, res: express.Response) {
   const result = await buildMutedForSearchIndex(res)
 
-  let body: VideosSearchQuery = Object.assign(query, result)
+  let body = { ...query, ...result }
 
   // Use the default instance NSFW policy if not specified
   if (!body.nsfw) {
@@ -98,13 +98,18 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
   }
 }
 
-async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
-  const apiOptions = await Hooks.wrapObject(Object.assign(query, {
+async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: express.Response) {
+  const apiOptions = await Hooks.wrapObject({
+    ...query,
+
     includeLocalVideos: true,
-    nsfw: buildNSFWFilter(res, query.nsfw),
     filter: query.filter,
-    user: res.locals.oauth ? res.locals.oauth.token.User : undefined
-  }), 'filter:api.search.videos.local.list.params')
+
+    nsfw: buildNSFWFilter(res, query.nsfw),
+    user: res.locals.oauth
+      ? res.locals.oauth.token.User
+      : undefined
+  }, 'filter:api.search.videos.local.list.params')
 
   const resultList = await Hooks.wrapPromiseFun(
     VideoModel.searchAndPopulateAccountAndServer,
index caddc0909b39616f8a366597776f5baf0256eca4..b315e99cf244aa6d69cac891b6b9cc440a6550bc 100644 (file)
@@ -1,9 +1,9 @@
 import * as express from 'express'
-import { asyncMiddleware, contactAdministratorValidator } from '../../../middlewares'
-import { Redis } from '../../../lib/redis'
-import { Emailer } from '../../../lib/emailer'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { ContactForm } from '../../../../shared/models/server'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { Emailer } from '../../../lib/emailer'
+import { Redis } from '../../../lib/redis'
+import { asyncMiddleware, contactAdministratorValidator } from '../../../middlewares'
 
 const contactRouter = express.Router()
 
@@ -15,7 +15,7 @@ contactRouter.post('/contact',
 async function contactAdministrator (req: express.Request, res: express.Response) {
   const data = req.body as ContactForm
 
-  await Emailer.Instance.addContactFormJob(data.fromEmail, data.fromName, data.subject, data.body)
+  Emailer.Instance.addContactFormJob(data.fromEmail, data.fromName, data.subject, data.body)
 
   await Redis.Instance.setContactFormIp(req.ip)
 
index a6e9147f3c170c984af1066d478a61d108674d12..0601b89ceb042c2e163223e34959adcc204216ad 100644 (file)
@@ -1,8 +1,8 @@
+import * as express from 'express'
 import { InboxManager } from '@server/lib/activitypub/inbox-manager'
 import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import { SendDebugCommand } from '@shared/models'
-import * as express from 'express'
+import { Debug, SendDebugCommand } from '@shared/models'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { UserRight } from '../../../../shared/models/users'
 import { authenticate, ensureUserHasRight } from '../../../middlewares'
 
@@ -32,7 +32,7 @@ function getDebug (req: express.Request, res: express.Response) {
   return res.json({
     ip: req.ip,
     activityPubMessagesWaiting: InboxManager.Instance.getActivityPubMessagesWaiting()
-  })
+  } as Debug)
 }
 
 async function runCommand (req: express.Request, res: express.Response) {
index 12357a2ca9e483023234d6cc9746204bf66ab80b..cbe6b7e4f5c5a75237d66ca8b1eeeb51454b9c6f 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { getServerActor } from '@server/models/application/application'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { UserRight } from '../../../../shared/models/users'
 import { logger } from '../../../helpers/logger'
 import { getFormattedObjects } from '../../../helpers/utils'
@@ -29,6 +29,7 @@ import {
   removeFollowingValidator
 } from '../../../middlewares/validators'
 import { ActorFollowModel } from '../../../models/actor/actor-follow'
+import { ServerFollowCreate } from '@shared/models'
 
 const serverFollowsRouter = express.Router()
 serverFollowsRouter.get('/following',
@@ -45,10 +46,10 @@ serverFollowsRouter.post('/following',
   ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
   followValidator,
   setBodyHostsPort,
-  asyncMiddleware(followInstance)
+  asyncMiddleware(addFollow)
 )
 
-serverFollowsRouter.delete('/following/:host',
+serverFollowsRouter.delete('/following/:hostOrHandle',
   authenticate,
   ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
   asyncMiddleware(removeFollowingValidator),
@@ -125,8 +126,8 @@ async function listFollowers (req: express.Request, res: express.Response) {
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
 
-async function followInstance (req: express.Request, res: express.Response) {
-  const hosts = req.body.hosts as string[]
+async function addFollow (req: express.Request, res: express.Response) {
+  const { hosts, handles } = req.body as ServerFollowCreate
   const follower = await getServerActor()
 
   for (const host of hosts) {
@@ -139,6 +140,18 @@ async function followInstance (req: express.Request, res: express.Response) {
     JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
   }
 
+  for (const handle of handles) {
+    const [ name, host ] = handle.split('@')
+
+    const payload = {
+      host,
+      name,
+      followerActorId: follower.id
+    }
+
+    JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
+  }
+
   return res.status(HttpStatusCode.NO_CONTENT_204).end()
 }
 
index 6b8793a196d108fecf646db9c77eb40e865bc447..32fefefc04c90893dc42516872edcba87d22d918 100644 (file)
@@ -1,11 +1,11 @@
 import * as express from 'express'
+import { contactRouter } from './contact'
+import { debugRouter } from './debug'
 import { serverFollowsRouter } from './follows'
-import { statsRouter } from './stats'
+import { logsRouter } from './logs'
 import { serverRedundancyRouter } from './redundancy'
 import { serverBlocklistRouter } from './server-blocklist'
-import { contactRouter } from './contact'
-import { logsRouter } from './logs'
-import { debugRouter } from './debug'
+import { statsRouter } from './stats'
 
 const serverRouter = express.Router()
 
index 4b543d686e45512bf2fdec2ceff03ccd1d594795..39eceb6547d7c4902de199c3f568ddc380672690 100644 (file)
@@ -1,14 +1,13 @@
 import * as express from 'express'
-import { UserRight } from '../../../../shared/models/users'
-import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
-import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs'
 import { readdir, readFile } from 'fs-extra'
-import { AUDIT_LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS, LOG_FILENAME } from '../../../initializers/constants'
 import { join } from 'path'
-import { getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs'
+import { logger, mtimeSortFilesDesc } from '@server/helpers/logger'
 import { LogLevel } from '../../../../shared/models/server/log-level.type'
+import { UserRight } from '../../../../shared/models/users'
 import { CONFIG } from '../../../initializers/config'
-import { logger } from '@server/helpers/logger'
+import { AUDIT_LOG_FILENAME, LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants'
+import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
+import { getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs'
 
 const logsRouter = express.Router()
 
index bc593ad4359a1f348139ff8791250c0640d36cac..99d1c762b8d1d8f3bff98259c74f3c122bdbc724 100644 (file)
@@ -1,5 +1,10 @@
 import * as express from 'express'
+import { JobQueue } from '@server/lib/job-queue'
+import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { UserRight } from '../../../../shared/models/users'
+import { logger } from '../../../helpers/logger'
+import { removeRedundanciesOfServer, removeVideoRedundancy } from '../../../lib/redundancy'
 import {
   asyncMiddleware,
   authenticate,
@@ -10,16 +15,11 @@ import {
   videoRedundanciesSortValidator
 } from '../../../middlewares'
 import {
-  listVideoRedundanciesValidator,
-  updateServerRedundancyValidator,
   addVideoRedundancyValidator,
-  removeVideoRedundancyValidator
+  listVideoRedundanciesValidator,
+  removeVideoRedundancyValidator,
+  updateServerRedundancyValidator
 } from '../../../middlewares/validators/redundancy'
-import { removeRedundanciesOfServer, removeVideoRedundancy } from '../../../lib/redundancy'
-import { logger } from '../../../helpers/logger'
-import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
-import { JobQueue } from '@server/lib/job-queue'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const serverRedundancyRouter = express.Router()
 
index a86bc7d191f333dd255b21559a94da1a54814ad8..b3ee50d855947e805d928d950c972f692a37baf2 100644 (file)
@@ -1,8 +1,9 @@
 import 'multer'
 import * as express from 'express'
 import { logger } from '@server/helpers/logger'
-import { UserNotificationModel } from '@server/models/user/user-notification'
 import { getServerActor } from '@server/models/application/application'
+import { UserNotificationModel } from '@server/models/user/user-notification'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { UserRight } from '../../../../shared/models/users'
 import { getFormattedObjects } from '../../../helpers/utils'
 import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
@@ -25,7 +26,6 @@ import {
 } from '../../../middlewares/validators'
 import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
 import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const serverBlocklistRouter = express.Router()
 
index 3aea124505365987769059a8e5004512976266bc..397702548b0e54ff992acf08a75cc972643fd185 100644 (file)
@@ -2,12 +2,12 @@ import * as express from 'express'
 import { StatsManager } from '@server/lib/stat-manager'
 import { ROUTE_CACHE_LIFETIME } from '../../../initializers/constants'
 import { asyncMiddleware } from '../../../middlewares'
-import { cacheRoute } from '../../../middlewares/cache'
+import { cacheRoute } from '../../../middlewares/cache/cache'
 
 const statsRouter = express.Router()
 
 statsRouter.get('/stats',
-  asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.STATS)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.STATS),
   asyncMiddleware(getStats)
 )
 
index d907b49bf15123eb786ad5c201e1979924d37fea..be800e8b5d5e0a9589883ad70de9a323df1fa3cd 100644 (file)
@@ -4,8 +4,8 @@ import { tokensRouter } from '@server/controllers/api/users/token'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
 import { MUser, MUserAccountDefault } from '@server/types/models'
-import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { UserCreate, UserCreateResult, UserRight, UserRole, UserUpdate } from '../../../../shared'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
 import { UserRegister } from '../../../../shared/models/users/user-register.model'
 import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@@ -220,7 +220,7 @@ async function createUser (req: express.Request, res: express.Response) {
       account: {
         id: account.id
       }
-    }
+    } as UserCreateResult
   })
 }
 
index 1f2b2f9dde17ebf0f3d6c234006cdbe682d59b4b..ac6faca9cdae6e02a27f2976ae50d7a5783cb35c 100644 (file)
@@ -2,8 +2,9 @@ import 'multer'
 import * as express from 'express'
 import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger'
 import { Hooks } from '@server/lib/plugins/hooks'
+import { AttributesOnly } from '@shared/core-utils'
 import { ActorImageType, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
 import { createReqFiles } from '../../../helpers/express-utils'
 import { getFormattedObjects } from '../../../helpers/utils'
@@ -31,7 +32,6 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
 import { UserModel } from '../../../models/user/user'
 import { VideoModel } from '../../../models/video/video'
 import { VideoImportModel } from '../../../models/video/video-import'
-import { AttributesOnly } from '@shared/core-utils'
 
 const auditLogger = auditLoggerFactory('users')
 
index a1561b751b60dccc9c97a7796a4d17e6b23ffc69..24fff83e332a38eb989e2b5788d605a70d8efea7 100644 (file)
@@ -1,6 +1,10 @@
-import * as express from 'express'
 import 'multer'
+import * as express from 'express'
+import { logger } from '@server/helpers/logger'
+import { UserNotificationModel } from '@server/models/user/user-notification'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { getFormattedObjects } from '../../../helpers/utils'
+import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
@@ -18,11 +22,7 @@ import {
   unblockServerByAccountValidator
 } from '../../../middlewares/validators'
 import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
-import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
 import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
-import { UserNotificationModel } from '@server/models/user/user-notification'
-import { logger } from '@server/helpers/logger'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const myBlocklistRouter = express.Router()
 
index cff1697ab719320ba07ad5b54708011f5ec8cccf..a6e7231030e2d3d480235c4a3c00ed5d1ce5643f 100644 (file)
@@ -1,4 +1,7 @@
 import * as express from 'express'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
+import { getFormattedObjects } from '../../../helpers/utils'
+import { sequelizeTypescript } from '../../../initializers/database'
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
@@ -8,10 +11,7 @@ import {
   userHistoryListValidator,
   userHistoryRemoveValidator
 } from '../../../middlewares'
-import { getFormattedObjects } from '../../../helpers/utils'
 import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
-import { sequelizeTypescript } from '../../../initializers/database'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const myVideosHistoryRouter = express.Router()
 
index 2909770daed97391f59511b28f21e85338e934c0..3beee07c021634d3c773098b52a7450e9ca71ef6 100644 (file)
@@ -1,7 +1,7 @@
 import 'multer'
 import * as express from 'express'
 import { UserNotificationModel } from '@server/models/user/user-notification'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { UserNotificationSetting } from '../../../../shared/models/users'
 import { getFormattedObjects } from '../../../helpers/utils'
 import {
index 46a73d49e7cab87db551b2ac7f05c673a33c8221..26a715704b66a882315d27ad6c3aa8d5a7259c87 100644 (file)
@@ -1,9 +1,9 @@
 import 'multer'
 import * as express from 'express'
+import { pickCommonVideoQuery } from '@server/helpers/query'
 import { sendUndoFollow } from '@server/lib/activitypub/send'
 import { VideoChannelModel } from '@server/models/video/video-channel'
-import { VideosCommonQuery } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
 import { getFormattedObjects } from '../../../helpers/utils'
 import { WEBSERVER } from '../../../initializers/constants'
@@ -170,20 +170,13 @@ async function getUserSubscriptions (req: express.Request, res: express.Response
 async function getUserSubscriptionVideos (req: express.Request, res: express.Response) {
   const user = res.locals.oauth.token.User
   const countVideos = getCountVideos(req)
-  const query = req.query as VideosCommonQuery
+  const query = pickCommonVideoQuery(req.query)
 
   const resultList = await VideoModel.listForApi({
-    start: query.start,
-    count: query.count,
-    sort: query.sort,
+    ...query,
+
     includeLocalVideos: false,
-    categoryOneOf: query.categoryOneOf,
-    licenceOneOf: query.licenceOneOf,
-    languageOneOf: query.languageOneOf,
-    tagsOneOf: query.tagsOneOf,
-    tagsAllOf: query.tagsAllOf,
     nsfw: buildNSFWFilter(res, query.nsfw),
-    filter: query.filter,
     withFiles: false,
     followerActorId: user.Account.Actor.id,
     user,
index d0bd9946323b16066006c87f1f91f7d69099674d..76e741ba548e53a64197f163311e870fdb6f5e83 100644 (file)
@@ -1,8 +1,8 @@
 import * as express from 'express'
+import { VideosExistInPlaylists } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model'
 import { asyncMiddleware, authenticate } from '../../../middlewares'
 import { doVideosInPlaylistExistValidator } from '../../../middlewares/validators/videos/video-playlists'
 import { VideoPlaylistModel } from '../../../models/video/video-playlist'
-import { VideosExistInPlaylists } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model'
 
 const myVideoPlaylistsRouter = express.Router()
 
index bc8d203b041253dfe789539c2b3dfe2c16ac2365..7bdb337370f1145692c41b179031359677f73b1d 100644 (file)
@@ -1,9 +1,10 @@
 import * as express from 'express'
+import { pickCommonVideoQuery } from '@server/helpers/query'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { getServerActor } from '@server/models/application/application'
 import { MChannelBannerAccountDefault } from '@server/types/models'
-import { ActorImageType, VideoChannelCreate, VideoChannelUpdate, VideosCommonQuery } from '../../../shared'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
 import { resetSequelizeInstance } from '../../helpers/database-utils'
 import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
@@ -309,20 +310,13 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
   const videoChannelInstance = res.locals.videoChannel
   const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
   const countVideos = getCountVideos(req)
-  const query = req.query as VideosCommonQuery
+  const query = pickCommonVideoQuery(req.query)
 
   const apiOptions = await Hooks.wrapObject({
+    ...query,
+
     followerActorId,
-    start: query.start,
-    count: query.count,
-    sort: query.sort,
     includeLocalVideos: true,
-    categoryOneOf: query.categoryOneOf,
-    licenceOneOf: query.licenceOneOf,
-    languageOneOf: query.languageOneOf,
-    tagsOneOf: query.tagsOneOf,
-    tagsAllOf: query.tagsAllOf,
-    filter: query.filter,
     nsfw: buildNSFWFilter(res, query.nsfw),
     withFiles: false,
     videoChannelId: videoChannelInstance.id,
index 87a6f6bbee4a94c790ba63df586e255fe50453a6..4971d0a772eb9701a94054f447fe04bc3e401a12 100644 (file)
@@ -5,7 +5,8 @@ import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { getServerActor } from '@server/models/application/application'
 import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { VideoPlaylistCreateResult, VideoPlaylistElementCreateResult } from '@shared/models'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model'
 import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
 import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
@@ -202,7 +203,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
       id: videoPlaylistCreated.id,
       shortUUID: uuidToShort(videoPlaylistCreated.uuid),
       uuid: videoPlaylistCreated.uuid
-    }
+    } as VideoPlaylistCreateResult
   })
 }
 
@@ -338,8 +339,8 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
   return res.json({
     videoPlaylistElement: {
       id: playlistElement.id
-    }
-  }).end()
+    } as VideoPlaylistElementCreateResult
+  })
 }
 
 async function updateVideoPlaylistElement (req: express.Request, res: express.Response) {
index 530e179656e98a71163bc994502a7cf175037d2a..6bc768471cb7e7bda84540136d4cb0267f299cbb 100644 (file)
@@ -1,6 +1,7 @@
 import * as express from 'express'
 import { blacklistVideo, unblacklistVideo } from '@server/lib/video-blacklist'
 import { UserRight, VideoBlacklistCreate } from '../../../../shared'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { logger } from '../../../helpers/logger'
 import { getFormattedObjects } from '../../../helpers/utils'
 import { sequelizeTypescript } from '../../../initializers/database'
@@ -19,7 +20,6 @@ import {
   videosBlacklistUpdateValidator
 } from '../../../middlewares'
 import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const blacklistRouter = express.Router()
 
index ad7423a31fb916834713c907f531f39fa0ed4f58..4008de60fc59c2a12ddf175383e63cdd218a26ab 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { MVideoCaption } from '@server/types/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
 import { createReqFiles } from '../../../helpers/express-utils'
 import { logger } from '../../../helpers/logger'
index e6f28c1cb058a4bf1e755d8ed0867fbf949dded6..cb696f652cc669b84d9a49798935ac6ceae00b6b 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import { ResultList, ThreadsResultList, UserRight } from '../../../../shared/models'
-import { VideoCommentCreate } from '../../../../shared/models/videos/comment/video-comment.model'
+import { ResultList, ThreadsResultList, UserRight, VideoCommentCreate } from '../../../../shared/models'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
+import { VideoCommentThreads } from '../../../../shared/models/videos/comment/video-comment.model'
 import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
 import { getFormattedObjects } from '../../../helpers/utils'
 import { sequelizeTypescript } from '../../../initializers/database'
@@ -136,7 +136,7 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
   return res.json({
     ...getFormattedObjects(resultList.data, resultList.total),
     totalNotDeletedComments: resultList.totalNotDeletedComments
-  })
+  } as VideoCommentThreads)
 }
 
 async function listVideoThreadComments (req: express.Request, res: express.Response) {
index 74b100e59249c15df2690665b4c3d0996791b414..49490f79b13c3579918f0f47d337d47cd88f97d1 100644 (file)
@@ -1,12 +1,12 @@
 import * as express from 'express'
 import toInt from 'validator/lib/toInt'
+import { pickCommonVideoQuery } from '@server/helpers/query'
 import { doJSONRequest } from '@server/helpers/requests'
 import { LiveManager } from '@server/lib/live'
 import { openapiOperationDoc } from '@server/middlewares/doc'
 import { getServerActor } from '@server/models/application/application'
 import { MVideoAccountLight } from '@server/types/models'
-import { VideosCommonQuery } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
+import { HttpStatusCode } from '../../../../shared/models'
 import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
 import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
 import { logger } from '../../../helpers/logger'
@@ -211,22 +211,14 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response
 }
 
 async function listVideos (req: express.Request, res: express.Response) {
-  const query = req.query as VideosCommonQuery
+  const query = pickCommonVideoQuery(req.query)
   const countVideos = getCountVideos(req)
 
   const apiOptions = await Hooks.wrapObject({
-    start: query.start,
-    count: query.count,
-    sort: query.sort,
+    ...query,
+
     includeLocalVideos: true,
-    categoryOneOf: query.categoryOneOf,
-    licenceOneOf: query.licenceOneOf,
-    languageOneOf: query.languageOneOf,
-    tagsOneOf: query.tagsOneOf,
-    tagsAllOf: query.tagsAllOf,
     nsfw: buildNSFWFilter(res, query.nsfw),
-    isLive: query.isLive,
-    filter: query.filter,
     withFiles: false,
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
     countVideos
index d8c51c2d41b5c87e54d3ea3b6ddd6428eb267eea..ed4da8f479ad3154bd545e57b90d88872256b7fe 100644 (file)
@@ -11,7 +11,7 @@ import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator
 import { VideoLiveModel } from '@server/models/video/video-live'
 import { MVideoDetails, MVideoFullLight } from '@server/types/models'
 import { LiveVideoCreate, LiveVideoUpdate, VideoState } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { logger } from '../../../helpers/logger'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
index 1bb96e0460260801a39ec0c786f662683c9f023a..f48acbc681843190eb2b39051b4736218fbc2d28 100644 (file)
@@ -1,6 +1,12 @@
 import * as express from 'express'
+import { MVideoFullLight } from '@server/types/models'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
+import { VideoChangeOwnershipStatus, VideoState } from '../../../../shared/models/videos'
 import { logger } from '../../../helpers/logger'
+import { getFormattedObjects } from '../../../helpers/utils'
 import { sequelizeTypescript } from '../../../initializers/database'
+import { sendUpdateVideo } from '../../../lib/activitypub/send'
+import { changeVideoChannelShare } from '../../../lib/activitypub/share'
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
@@ -11,15 +17,9 @@ import {
   videosChangeOwnershipValidator,
   videosTerminateChangeOwnershipValidator
 } from '../../../middlewares'
+import { VideoModel } from '../../../models/video/video'
 import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
-import { VideoChangeOwnershipStatus, VideoState } from '../../../../shared/models/videos'
 import { VideoChannelModel } from '../../../models/video/video-channel'
-import { getFormattedObjects } from '../../../helpers/utils'
-import { changeVideoChannelShare } from '../../../lib/activitypub/share'
-import { sendUpdateVideo } from '../../../lib/activitypub/send'
-import { VideoModel } from '../../../models/video/video'
-import { MVideoFullLight } from '@server/types/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const ownershipVideoRouter = express.Router()
 
index 84f42633e7b612689e860f15a6c7cc37df213ff4..96f6cd886bf7726f6f3346344df2624329573976 100644 (file)
@@ -1,13 +1,13 @@
 import * as express from 'express'
 import { UserVideoRateUpdate } from '../../../../shared'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { logger } from '../../../helpers/logger'
 import { VIDEO_RATE_TYPES } from '../../../initializers/constants'
+import { sequelizeTypescript } from '../../../initializers/database'
 import { getLocalRateUrl, sendVideoRateChange } from '../../../lib/activitypub/video-rates'
 import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
 import { AccountModel } from '../../../models/account/account'
 import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
-import { sequelizeTypescript } from '../../../initializers/database'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const rateVideoRouter = express.Router()
 
index 8affe71c682afa9048b825c7df592599fa67131f..49639060bdd3f1d4559550f43fe468e439d0fa37 100644 (file)
@@ -2,10 +2,11 @@ import * as express from 'express'
 import { Transaction } from 'sequelize/types'
 import { changeVideoChannelShare } from '@server/lib/activitypub/share'
 import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
+import { openapiOperationDoc } from '@server/middlewares/doc'
 import { FilteredModelAttributes } from '@server/types'
 import { MVideoFullLight } from '@server/types/models'
 import { VideoUpdate } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
+import { HttpStatusCode } from '../../../../shared/models'
 import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
 import { resetSequelizeInstance } from '../../../helpers/database-utils'
 import { createReqFiles } from '../../../helpers/express-utils'
@@ -20,7 +21,6 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
 import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares'
 import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
 import { VideoModel } from '../../../models/video/video'
-import { openapiOperationDoc } from '@server/middlewares/doc'
 
 const lTags = loggerTagsFactory('api', 'video')
 const auditLogger = auditLoggerFactory('videos')
index bcd21ac9987eeb83b40febb92cac7f475af3f507..408f677ffd474ea9b08eab2d0edb8fe0354e71fd 100644 (file)
@@ -6,12 +6,12 @@ import { uuidToShort } from '@server/helpers/uuid'
 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
 import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
-import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
+import { generateWebTorrentVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 import { openapiOperationDoc } from '@server/middlewares/doc'
 import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
 import { uploadx } from '@uploadx/core'
 import { VideoCreate, VideoState } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
+import { HttpStatusCode } from '../../../../shared/models'
 import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { createReqFiles } from '../../../helpers/express-utils'
@@ -209,10 +209,12 @@ async function addVideo (options: {
   })
 
   createTorrentFederate(video, videoFile)
+    .then(() => {
+      if (video.state !== VideoState.TO_TRANSCODE) return
 
-  if (video.state === VideoState.TO_TRANSCODE) {
-    await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
-  }
+      return addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
+    })
+    .catch(err => logger.error('Cannot add optimize/merge audio job for %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) }))
 
   Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
 
@@ -240,7 +242,7 @@ async function buildNewFile (video: MVideo, videoPhysicalFile: express.VideoUplo
     videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
   }
 
-  videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)
+  videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname)
 
   return videoFile
 }
@@ -259,9 +261,9 @@ async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoF
   return refreshedFile.save()
 }
 
-function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile): void {
+function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile) {
   // Create the torrent file in async way because it could be long
-  createTorrentAndSetInfoHashAsync(video, videoFile)
+  return createTorrentAndSetInfoHashAsync(video, videoFile)
     .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
     .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
     .then(refreshedVideo => {
index 8b15525aa5d20ab71ceb04dcaea5bfadaca9bc7c..05c75e543c9d87dff05a2e0c5bd254755748a671 100644 (file)
@@ -1,5 +1,6 @@
 import * as express from 'express'
 import { UserWatchingVideo } from '../../../../shared'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
@@ -8,7 +9,6 @@ import {
   videoWatchingValidator
 } from '../../../middlewares'
 import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const watchingRouter = express.Router()
 
index 9e92063d4ecf1be651ece80990ef35a845b38c62..de04116086ee2fb6257713f3360ffacf5cec0e9e 100644 (file)
@@ -1,20 +1,20 @@
 import * as express from 'express'
-import { asyncMiddleware } from '../middlewares'
-import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
+import { truncate } from 'lodash'
 import { SitemapStream, streamToPromise } from 'sitemap'
+import { buildNSFWFilter } from '../helpers/express-utils'
+import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
+import { asyncMiddleware } from '../middlewares'
+import { cacheRoute } from '../middlewares/cache/cache'
+import { AccountModel } from '../models/account/account'
 import { VideoModel } from '../models/video/video'
 import { VideoChannelModel } from '../models/video/video-channel'
-import { AccountModel } from '../models/account/account'
-import { cacheRoute } from '../middlewares/cache'
-import { buildNSFWFilter } from '../helpers/express-utils'
-import { truncate } from 'lodash'
 
 const botsRouter = express.Router()
 
 // Special route that add OpenGraph and oEmbed tags
 // Do not use a template engine for a so little thing
 botsRouter.use('/sitemap.xml',
-  asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.SITEMAP)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP),
   asyncMiddleware(getSitemap)
 )
 
@@ -75,13 +75,13 @@ async function getSitemapLocalVideoUrls () {
   })
 
   return data.map(v => ({
-    url: WEBSERVER.URL + '/w/' + v.uuid,
+    url: WEBSERVER.URL + v.getWatchStaticPath(),
     video: [
       {
         title: v.name,
         // Sitemap description should be < 2000 characters
         description: truncate(v.description || v.name, { length: 2000, omission: '...' }),
-        player_loc: WEBSERVER.URL + '/videos/embed/' + v.uuid,
+        player_loc: WEBSERVER.URL + v.getEmbedStaticPath(),
         thumbnail_loc: WEBSERVER.URL + v.getMiniatureStaticPath()
       }
     ]
index eb1ee6cbd6e8e6e546cbb118003a9599ea071dc6..ba3c54440a43d20d93aedee0c52afd94e477fdd0 100644 (file)
@@ -5,7 +5,7 @@ import { join } from 'path'
 import { logger } from '@server/helpers/logger'
 import { CONFIG } from '@server/initializers/config'
 import { Hooks } from '@server/lib/plugins/hooks'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n'
 import { root } from '../helpers/core-utils'
 import { STATIC_MAX_AGE } from '../initializers/constants'
index 4293a32e2be5b8a815a5e5701bcc14cc886e03f6..ddacc1b68ac13ed15afb4fc9ca1dd1b43b1e65dc 100644 (file)
@@ -5,8 +5,7 @@ import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache
 import { Hooks } from '@server/lib/plugins/hooks'
 import { getVideoFilePath } from '@server/lib/video-paths'
 import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { VideoStreamingPlaylistType } from '@shared/models'
+import { HttpStatusCode, VideoStreamingPlaylistType } from '@shared/models'
 import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
 import { asyncMiddleware, videosDownloadValidator } from '../middlewares'
 
index 435b12193a9bee8fc0cf823aa0ec8f7be73dbdd3..9fa70a7c8ca89e7f9cc9983f422252a909801517 100644 (file)
@@ -16,20 +16,20 @@ import {
   videosSortValidator,
   videoSubscriptionFeedsValidator
 } from '../middlewares'
-import { cacheRoute } from '../middlewares/cache'
+import { cacheRouteFactory } from '../middlewares/cache/cache'
 import { VideoModel } from '../models/video/video'
 import { VideoCommentModel } from '../models/video/video-comment'
 
 const feedsRouter = express.Router()
 
+const cacheRoute = cacheRouteFactory({
+  headerBlacklist: [ 'Content-Type' ]
+})
+
 feedsRouter.get('/feeds/video-comments.:format',
   feedsFormatValidator,
   setFeedFormatContentType,
-  asyncMiddleware(cacheRoute({
-    headerBlacklist: [
-      'Content-Type'
-    ]
-  })(ROUTE_CACHE_LIFETIME.FEEDS)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
   asyncMiddleware(videoFeedsValidator),
   asyncMiddleware(videoCommentsFeedsValidator),
   asyncMiddleware(generateVideoCommentsFeed)
@@ -40,11 +40,7 @@ feedsRouter.get('/feeds/videos.:format',
   setDefaultVideosSort,
   feedsFormatValidator,
   setFeedFormatContentType,
-  asyncMiddleware(cacheRoute({
-    headerBlacklist: [
-      'Content-Type'
-    ]
-  })(ROUTE_CACHE_LIFETIME.FEEDS)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
   commonVideosFiltersValidator,
   asyncMiddleware(videoFeedsValidator),
   asyncMiddleware(generateVideoFeed)
@@ -55,11 +51,7 @@ feedsRouter.get('/feeds/subscriptions.:format',
   setDefaultVideosSort,
   feedsFormatValidator,
   setFeedFormatContentType,
-  asyncMiddleware(cacheRoute({
-    headerBlacklist: [
-      'Content-Type'
-    ]
-  })(ROUTE_CACHE_LIFETIME.FEEDS)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
   commonVideosFiltersValidator,
   asyncMiddleware(videoSubscriptionFeedsValidator),
   asyncMiddleware(generateVideoFeedForSubscriptions)
@@ -294,7 +286,7 @@ function addVideosToFeed (feed, videos: VideoModel[]) {
     feed.addItem({
       title: video.name,
       id: video.url,
-      link: WEBSERVER.URL + '/w/' + video.uuid,
+      link: WEBSERVER.URL + video.getWatchStaticPath(),
       description: video.getTruncatedDescription(),
       content: video.description,
       author: [
index 9a7dacba049e86d3eb546dcc4bc91369ff591333..632e4dcd8d6f5b7f5d72368541e6cb6e6f3b014c 100644 (file)
@@ -1,7 +1,7 @@
 import * as cors from 'cors'
 import * as express from 'express'
 import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
 import { logger } from '../helpers/logger'
 import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
 import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
index f2686fb237d9b3dd4528eb7fbd9996298a34920a..95d5c01357984eafdb51603ed0cc9d583e118d01 100644 (file)
@@ -2,7 +2,7 @@ import * as cors from 'cors'
 import * as express from 'express'
 import { mapToJSON } from '@server/helpers/core-utils'
 import { LiveSegmentShaStore } from '@server/lib/live'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '@shared/models'
 
 const liveRouter = express.Router()
 
index 7213e3f15fd6ec1c06ed7deaff67ad9c0c8758c5..11ab3f10ab47ee7b39c85aef2c034d08078414f9 100644 (file)
@@ -3,7 +3,7 @@ import { join } from 'path'
 import { logger } from '@server/helpers/logger'
 import { optionalAuthenticate } from '@server/middlewares/auth'
 import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
 import { PluginType } from '../../shared/models/plugins/plugin.type'
 import { isTestInstance } from '../helpers/core-utils'
 import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
index 35e024dda2db9b11df45c1195c900219442dd829..912d7e36c8265b804e1d928ea83187ee33fb1891 100644 (file)
@@ -3,7 +3,7 @@ import * as express from 'express'
 import { join } from 'path'
 import { serveIndexHTML } from '@server/lib/client-html'
 import { ServerConfigManager } from '@server/lib/server-config-manager'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '@shared/models'
 import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo/nodeinfo.model'
 import { root } from '../helpers/core-utils'
 import { CONFIG, isEmailEnabled } from '../initializers/config'
@@ -19,7 +19,7 @@ import {
 } from '../initializers/constants'
 import { getThemeOrDefault } from '../lib/plugins/theme-utils'
 import { asyncMiddleware } from '../middlewares'
-import { cacheRoute } from '../middlewares/cache'
+import { cacheRoute } from '../middlewares/cache/cache'
 import { UserModel } from '../models/user/user'
 import { VideoModel } from '../models/video/video'
 import { VideoCommentModel } from '../models/video/video-comment'
@@ -66,7 +66,7 @@ staticRouter.use(
 
 // robots.txt service
 staticRouter.get('/robots.txt',
-  asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.ROBOTS)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.ROBOTS),
   (_, res: express.Response) => {
     res.type('text/plain')
     return res.send(CONFIG.INSTANCE.ROBOTS)
@@ -86,7 +86,7 @@ staticRouter.get('/security.txt',
 )
 
 staticRouter.get('/.well-known/security.txt',
-  asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.SECURITYTXT)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.SECURITYTXT),
   (_, res: express.Response) => {
     res.type('text/plain')
     return res.send(CONFIG.INSTANCE.SECURITYTXT + CONFIG.INSTANCE.SECURITYTXT_CONTACT)
@@ -95,7 +95,7 @@ staticRouter.get('/.well-known/security.txt',
 
 // nodeinfo service
 staticRouter.use('/.well-known/nodeinfo',
-  asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.NODEINFO)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.NODEINFO),
   (_, res: express.Response) => {
     return res.json({
       links: [
@@ -108,13 +108,13 @@ staticRouter.use('/.well-known/nodeinfo',
   }
 )
 staticRouter.use('/nodeinfo/:version.json',
-  asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.NODEINFO)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.NODEINFO),
   asyncMiddleware(generateNodeinfo)
 )
 
 // dnt-policy.txt service (see https://www.eff.org/dnt-policy)
 staticRouter.use('/.well-known/dnt-policy.txt',
-  asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.DNT_POLICY)),
+  cacheRoute(ROUTE_CACHE_LIFETIME.DNT_POLICY),
   (_, res: express.Response) => {
     res.type('text/plain')
 
index fbef7ad8712d58a746eb2ea9671de86be91d16a8..8f65552c33ba48525972c0f6859076852b97dda6 100644 (file)
@@ -1,4 +1,4 @@
-import { exists } from './misc'
+import { exists, isArray } from './misc'
 import { FollowState } from '@shared/models'
 
 function isFollowStateValid (value: FollowState) {
@@ -7,8 +7,24 @@ function isFollowStateValid (value: FollowState) {
   return value === 'pending' || value === 'accepted'
 }
 
+function isRemoteHandleValid (value: string) {
+  if (!exists(value)) return false
+  if (typeof value !== 'string') return false
+
+  return value.includes('@')
+}
+
+function isEachUniqueHandleValid (handles: string[]) {
+  return isArray(handles) &&
+    handles.every(handle => {
+      return isRemoteHandleValid(handle) && handles.indexOf(handle) === handles.lastIndexOf(handle)
+    })
+}
+
 // ---------------------------------------------------------------------------
 
 export {
-  isFollowStateValid
+  isFollowStateValid,
+  isRemoteHandleValid,
+  isEachUniqueHandleValid
 }
index 528bfcfb851eec54d815177402ae0860c8e4c9eb..c19a3e5eba2692e7f1e87075d20509ede9bba365 100644 (file)
@@ -23,6 +23,10 @@ function isNotEmptyIntArray (value: any) {
   return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0
 }
 
+function isNotEmptyStringArray (value: any) {
+  return Array.isArray(value) && value.every(v => typeof v === 'string' && v.length !== 0) && value.length !== 0
+}
+
 function isArrayOf (value: any, validator: (value: any) => boolean) {
   return isArray(value) && value.every(v => validator(v))
 }
@@ -39,6 +43,10 @@ function isUUIDValid (value: string) {
   return exists(value) && validator.isUUID('' + value, 4)
 }
 
+function areUUIDsValid (values: string[]) {
+  return isArray(values) && values.every(v => isUUIDValid(v))
+}
+
 function isIdOrUUIDValid (value: string) {
   return isIdValid(value) || isUUIDValid(value)
 }
@@ -132,6 +140,10 @@ function toCompleteUUID (value: string) {
   return value
 }
 
+function toCompleteUUIDs (values: string[]) {
+  return values.map(v => toCompleteUUID(v))
+}
+
 function toIntOrNull (value: string) {
   const v = toValueOrNull(value)
 
@@ -179,7 +191,9 @@ export {
   isIntOrNull,
   isIdValid,
   isSafePath,
+  isNotEmptyStringArray,
   isUUIDValid,
+  toCompleteUUIDs,
   toCompleteUUID,
   isIdOrUUIDValid,
   isDateValid,
@@ -187,6 +201,7 @@ export {
   toBooleanOrNull,
   isBooleanValid,
   toIntOrNull,
+  areUUIDsValid,
   toArray,
   toIntArray,
   isFileFieldValid,
index adf1ea497e17ae092d73f8e00b74a55868cf7db5..c0f8b6aebb23219e86f0932ab13a0487822eb2a9 100644 (file)
@@ -19,7 +19,6 @@ function isHostValid (host: string) {
 
 function isEachUniqueHostValid (hosts: string[]) {
   return isArray(hosts) &&
-    hosts.length !== 0 &&
     hosts.every(host => {
       return isHostValid(host) && hosts.indexOf(host) === hosts.lastIndexOf(host)
     })
index 0e1c63bad7b9d8afb0d60aaa5ca839a0d17ddf45..cf15b385a7c850320214d9c9715535b36911dd91 100644 (file)
@@ -1,7 +1,7 @@
 import { Response } from 'express'
 import { MUserId } from '@server/types/models'
 import { MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 
 function checkUserCanTerminateOwnershipChange (user: MUserId, videoChangeOwnership: MVideoChangeOwnershipFull, res: Response) {
   if (videoChangeOwnership.NextOwner.userId === user.id) {
index b5dc70c17dbb33ae9871baf9754628259f95a185..ec35295df2fd0f470ffd50ce720ad310e6fc085b 100644 (file)
@@ -1,12 +1,12 @@
 import * as retry from 'async/retry'
 import * as Bluebird from 'bluebird'
-import { QueryTypes, Transaction } from 'sequelize'
+import { Transaction } from 'sequelize'
 import { Model } from 'sequelize-typescript'
 import { sequelizeTypescript } from '@server/initializers/database'
 import { logger } from './logger'
 
 function retryTransactionWrapper <T, A, B, C, D> (
-  functionToRetry: (arg1: A, arg2: B, arg3: C, arg4: D) => Promise<T> | Bluebird<T>,
+  functionToRetry: (arg1: A, arg2: B, arg3: C, arg4: D) => Promise<T>,
   arg1: A,
   arg2: B,
   arg3: C,
@@ -14,20 +14,20 @@ function retryTransactionWrapper <T, A, B, C, D> (
 ): Promise<T>
 
 function retryTransactionWrapper <T, A, B, C> (
-  functionToRetry: (arg1: A, arg2: B, arg3: C) => Promise<T> | Bluebird<T>,
+  functionToRetry: (arg1: A, arg2: B, arg3: C) => Promise<T>,
   arg1: A,
   arg2: B,
   arg3: C
 ): Promise<T>
 
 function retryTransactionWrapper <T, A, B> (
-  functionToRetry: (arg1: A, arg2: B) => Promise<T> | Bluebird<T>,
+  functionToRetry: (arg1: A, arg2: B) => Promise<T>,
   arg1: A,
   arg2: B
 ): Promise<T>
 
 function retryTransactionWrapper <T, A> (
-  functionToRetry: (arg1: A) => Promise<T> | Bluebird<T>,
+  functionToRetry: (arg1: A) => Promise<T>,
   arg1: A
 ): Promise<T>
 
@@ -36,7 +36,7 @@ function retryTransactionWrapper <T> (
 ): Promise<T>
 
 function retryTransactionWrapper <T> (
-  functionToRetry: (...args: any[]) => Promise<T> | Bluebird<T>,
+  functionToRetry: (...args: any[]) => Promise<T>,
   ...args: any[]
 ): Promise<T> {
   return transactionRetryer<T>(callback => {
@@ -84,25 +84,15 @@ function resetSequelizeInstance (instance: Model<any>, savedFields: object) {
   })
 }
 
-function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Pick<Model, 'destroy'>> (
+function filterNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean }> (
   fromDatabase: T[],
-  newModels: T[],
-  t: Transaction
+  newModels: T[]
 ) {
   return fromDatabase.filter(f => !newModels.find(newModel => newModel.hasSameUniqueKeysThan(f)))
-              .map(f => f.destroy({ transaction: t }))
 }
 
-// Sequelize always skip the update if we only update updatedAt field
-function setAsUpdated (table: string, id: number, transaction?: Transaction) {
-  return sequelizeTypescript.query(
-    `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`,
-    {
-      replacements: { table, id, updatedAt: new Date() },
-      type: QueryTypes.UPDATE,
-      transaction
-    }
-  )
+function deleteAllModels <T extends Pick<Model, 'destroy'>> (models: T[], transaction: Transaction) {
+  return Promise.all(models.map(f => f.destroy({ transaction })))
 }
 
 // ---------------------------------------------------------------------------
@@ -127,7 +117,7 @@ export {
   transactionRetryer,
   updateInstanceWithAnother,
   afterCommitIfTransaction,
-  deleteNonExistingModels,
-  setAsUpdated,
+  filterNonExistingModels,
+  deleteAllModels,
   runInReadCommittedTransaction
 }
index 0ff1132742504099a9d4a2ce5cf09e9318cfed9f..c299b70f139ef590432574fe57e524c1c55715e9 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import * as multer from 'multer'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
 import { CONFIG } from '../initializers/config'
 import { REMOTE_SCHEME } from '../initializers/constants'
 import { getLowercaseExtension } from './core-utils'
index 6f5a71b4a51663bf1b40acf1c1222a44f021f894..61c8a6db26bd83ac84e7c3ce8bea14913164135d 100644 (file)
@@ -212,14 +212,17 @@ async function transcode (options: TranscodeOptions) {
 
 async function getLiveTranscodingCommand (options: {
   rtmpUrl: string
+
   outPath: string
+  masterPlaylistName: string
+
   resolutions: number[]
   fps: number
 
   availableEncoders: AvailableEncoders
   profile: string
 }) {
-  const { rtmpUrl, outPath, resolutions, fps, availableEncoders, profile } = options
+  const { rtmpUrl, outPath, resolutions, fps, availableEncoders, profile, masterPlaylistName } = options
   const input = rtmpUrl
 
   const command = getFFmpeg(input, 'live')
@@ -301,14 +304,14 @@ async function getLiveTranscodingCommand (options: {
 
   command.complexFilter(complexFilter)
 
-  addDefaultLiveHLSParams(command, outPath)
+  addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
 
   command.outputOption('-var_stream_map', varStreamMap.join(' '))
 
   return command
 }
 
-function getLiveMuxingCommand (rtmpUrl: string, outPath: string) {
+function getLiveMuxingCommand (rtmpUrl: string, outPath: string, masterPlaylistName: string) {
   const command = getFFmpeg(rtmpUrl, 'live')
 
   command.outputOption('-c:v copy')
@@ -316,7 +319,7 @@ function getLiveMuxingCommand (rtmpUrl: string, outPath: string) {
   command.outputOption('-map 0:a?')
   command.outputOption('-map 0:v?')
 
-  addDefaultLiveHLSParams(command, outPath)
+  addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
 
   return command
 }
@@ -371,12 +374,12 @@ function addDefaultEncoderParams (options: {
   }
 }
 
-function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string) {
+function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, masterPlaylistName: string) {
   command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS)
   command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
   command.outputOption('-hls_flags delete_segments+independent_segments')
   command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
-  command.outputOption('-master_pl_name master.m3u8')
+  command.outputOption('-master_pl_name ' + masterPlaylistName)
   command.outputOption(`-f hls`)
 
   command.output(join(outPath, '%v.m3u8'))
@@ -700,6 +703,10 @@ async function runCommand (options: {
   const { command, silent = false, job } = options
 
   return new Promise<void>((res, rej) => {
+    let shellCommand: string
+
+    command.on('start', cmdline => { shellCommand = cmdline })
+
     command.on('error', (err, stdout, stderr) => {
       if (silent !== true) logger.error('Error in ffmpeg.', { stdout, stderr })
 
@@ -707,7 +714,7 @@ async function runCommand (options: {
     })
 
     command.on('end', (stdout, stderr) => {
-      logger.debug('FFmpeg command ended.', { stdout, stderr })
+      logger.debug('FFmpeg command ended.', { stdout, stderr, shellCommand })
 
       res()
     })
index 29e06860dbf9ca0604805dc94bfeab2e75aa1d46..20c3c3edbaa111bb5ef304a98ae7781fb5648ebd 100644 (file)
@@ -1,5 +1,5 @@
 // Thanks http://tostring.it/2014/06/23/advanced-logging-with-nodejs/
-import { mkdirpSync } from 'fs-extra'
+import { mkdirpSync, stat } from 'fs-extra'
 import { omit } from 'lodash'
 import * as path from 'path'
 import { format as sqlFormat } from 'sql-formatter'
@@ -158,6 +158,26 @@ function loggerTagsFactory (...defaultTags: string[]): LoggerTagsFn {
   }
 }
 
+async function mtimeSortFilesDesc (files: string[], basePath: string) {
+  const promises = []
+  const out: { file: string, mtime: number }[] = []
+
+  for (const file of files) {
+    const p = stat(basePath + '/' + file)
+      .then(stats => {
+        if (stats.isFile()) out.push({ file, mtime: stats.mtime.getTime() })
+      })
+
+    promises.push(p)
+  }
+
+  await Promise.all(promises)
+
+  out.sort((a, b) => b.mtime - a.mtime)
+
+  return out
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -168,6 +188,7 @@ export {
   labelFormatter,
   consoleLoggerFormat,
   jsonLoggerFormat,
+  mtimeSortFilesDesc,
   logger,
   loggerTagsFactory,
   bunyanLogger
diff --git a/server/helpers/query.ts b/server/helpers/query.ts
new file mode 100644 (file)
index 0000000..e711b15
--- /dev/null
@@ -0,0 +1,74 @@
+import { pick } from '@shared/core-utils'
+import {
+  VideoChannelsSearchQueryAfterSanitize,
+  VideoPlaylistsSearchQueryAfterSanitize,
+  VideosCommonQueryAfterSanitize,
+  VideosSearchQueryAfterSanitize
+} from '@shared/models'
+
+function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
+  return pick(query, [
+    'start',
+    'count',
+    'sort',
+    'nsfw',
+    'isLive',
+    'categoryOneOf',
+    'licenceOneOf',
+    'languageOneOf',
+    'tagsOneOf',
+    'tagsAllOf',
+    'filter',
+    'skipCount'
+  ])
+}
+
+function pickSearchVideoQuery (query: VideosSearchQueryAfterSanitize) {
+  return {
+    ...pickCommonVideoQuery(query),
+
+    ...pick(query, [
+      'searchTarget',
+      'search',
+      'host',
+      'startDate',
+      'endDate',
+      'originallyPublishedStartDate',
+      'originallyPublishedEndDate',
+      'durationMin',
+      'durationMax',
+      'uuids'
+    ])
+  }
+}
+
+function pickSearchChannelQuery (query: VideoChannelsSearchQueryAfterSanitize) {
+  return pick(query, [
+    'searchTarget',
+    'search',
+    'start',
+    'count',
+    'sort',
+    'host',
+    'handles'
+  ])
+}
+
+function pickSearchPlaylistQuery (query: VideoPlaylistsSearchQueryAfterSanitize) {
+  return pick(query, [
+    'searchTarget',
+    'search',
+    'start',
+    'count',
+    'sort',
+    'host',
+    'uuids'
+  ])
+}
+
+export {
+  pickCommonVideoQuery,
+  pickSearchVideoQuery,
+  pickSearchPlaylistQuery,
+  pickSearchChannelQuery
+}
index d8220ba9c6583e3acb13e8ec7de8ba306a8fd983..ecf63e93e7434c421d4273cf6c34322c8bc30c1f 100644 (file)
@@ -103,6 +103,11 @@ async function createTorrentAndSetInfoHash (
 
   await writeFile(torrentPath, torrent)
 
+  // Remove old torrent file if it existed
+  if (videoFile.hasTorrent()) {
+    await remove(join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename))
+  }
+
   const parsedTorrent = parseTorrent(torrent)
   videoFile.infoHash = parsedTorrent.infoHash
   videoFile.torrentFilename = torrentFilename
index fdd36139002e6b8e02050d4e300e2fd91b6ea54c..3c80e7d41ccc10954ee6566e985a916a883ace30 100644 (file)
@@ -3,7 +3,7 @@ import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra'
 import got from 'got'
 import { join } from 'path'
 import { CONFIG } from '@server/initializers/config'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
 import { VideoResolution } from '../../shared/models/videos'
 import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants'
 import { peertubeTruncate, pipelinePromise, root } from './core-utils'
index ab59320ebce609e74eb3c489914195cc4ba09d66..5f121d9a4db6cb842a03ed812bbc4cb5ad46a8b8 100644 (file)
@@ -2,7 +2,7 @@ import { CronRepeatOptions, EveryRepeatOptions } from 'bull'
 import { randomBytes } from 'crypto'
 import { invert } from 'lodash'
 import { join } from 'path'
-import { randomInt } from '../../shared/core-utils/miscs/miscs'
+import { randomInt } from '../../shared/core-utils/common/miscs'
 import {
   AbuseState,
   JobType,
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 650
+const LAST_MIGRATION_VERSION = 655
 
 // ---------------------------------------------------------------------------
 
diff --git a/server/initializers/migrations/0655-streaming-playlist-filenames.ts b/server/initializers/migrations/0655-streaming-playlist-filenames.ts
new file mode 100644 (file)
index 0000000..9172a22
--- /dev/null
@@ -0,0 +1,66 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction
+  queryInterface: Sequelize.QueryInterface
+  sequelize: Sequelize.Sequelize
+  db: any
+}): Promise<void> {
+  {
+    for (const column of [ 'playlistUrl', 'segmentsSha256Url' ]) {
+      const data = {
+        type: Sequelize.STRING,
+        allowNull: true,
+        defaultValue: null
+      }
+
+      await utils.queryInterface.changeColumn('videoStreamingPlaylist', column, data)
+    }
+  }
+
+  {
+    await utils.sequelize.query(
+      `UPDATE "videoStreamingPlaylist" SET "playlistUrl" = NULL, "segmentsSha256Url" = NULL ` +
+      `WHERE "videoId" IN (SELECT id FROM video WHERE remote IS FALSE)`
+    )
+  }
+
+  {
+    for (const column of [ 'playlistFilename', 'segmentsSha256Filename' ]) {
+      const data = {
+        type: Sequelize.STRING,
+        allowNull: true,
+        defaultValue: null
+      }
+
+      await utils.queryInterface.addColumn('videoStreamingPlaylist', column, data)
+    }
+  }
+
+  {
+    await utils.sequelize.query(
+      `UPDATE "videoStreamingPlaylist" SET "playlistFilename" = 'master.m3u8', "segmentsSha256Filename" = 'segments-sha256.json'`
+    )
+  }
+
+  {
+    for (const column of [ 'playlistFilename', 'segmentsSha256Filename' ]) {
+      const data = {
+        type: Sequelize.STRING,
+        allowNull: false,
+        defaultValue: null
+      }
+
+      await utils.queryInterface.changeColumn('videoStreamingPlaylist', column, data)
+    }
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
index b2fe3932fff848d846912d813b023858b84426b5..0acaa9f62c1bfddb69ab9c7271d7032e00130837 100644 (file)
@@ -4,7 +4,7 @@ import { PeerTubeRequestError } from '@server/helpers/requests'
 import { ActorLoadByUrlType } from '@server/lib/model-loaders'
 import { ActorModel } from '@server/models/actor/actor'
 import { MActorAccountChannelId, MActorFull } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 import { fetchRemoteActor } from './shared'
 import { APActorUpdater } from './updater'
 import { getUrlFromWebfinger } from './webfinger'
index cd117f5712896e81ce374722723353a0e07ebbc8..28ff5225a01bde45b7ddf7b0e5086487bc713386 100644 (file)
@@ -1,3 +1,4 @@
+import { retryTransactionWrapper } from '@server/helpers/database-utils'
 import * as Bluebird from 'bluebird'
 import { URL } from 'url'
 import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
@@ -51,7 +52,7 @@ async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction
     }
   }
 
-  if (cleaner) await cleaner(startDate)
+  if (cleaner) await retryTransactionWrapper(cleaner, startDate)
 }
 
 export {
index c1bd667e04e1def55d048eeca0b5cf4b5aa16a5f..741b54df51be336ab0c988bd72d6d3dfcab4f23d 100644 (file)
@@ -31,6 +31,21 @@ async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors, transact
   }
 }
 
+// If we only have an host, use a default account handle
+function getRemoteNameAndHost (handleOrHost: string) {
+  let name = SERVER_ACTOR_NAME
+  let host = handleOrHost
+
+  const splitted = handleOrHost.split('@')
+  if (splitted.length === 2) {
+    name = splitted[0]
+    host = splitted[1]
+  }
+
+  return { name, host }
+}
+
 export {
-  autoFollowBackIfNeeded
+  autoFollowBackIfNeeded,
+  getRemoteNameAndHost
 }
index ef3cb3fe43db60c3c6fe9d12bd1525ec8bcc119c..493e8c7ec7bd6abbf8120aa26bb5579c3face4e8 100644 (file)
@@ -2,7 +2,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger'
 import { PeerTubeRequestError } from '@server/helpers/requests'
 import { JobQueue } from '@server/lib/job-queue'
 import { MVideoPlaylist, MVideoPlaylistOwner } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 import { createOrUpdateVideoPlaylist } from './create-update'
 import { fetchRemoteVideoPlaylist } from './shared'
 
index a7b82f2863b6d5dec25415365ff8f7e53933663b..3af08acf4d0b5642a9b23e1c1498cfa1ab8929db 100644 (file)
@@ -4,7 +4,7 @@ import { ActorFollowScoreCache } from '@server/lib/files-cache'
 import { VideoLoadByUrlType } from '@server/lib/model-loaders'
 import { VideoModel } from '@server/models/video/video'
 import { MVideoAccountLightBlacklistAllFiles, MVideoThumbnail } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 import { fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared'
 import { APVideoUpdater } from './updater'
 
index e89c94bcd22bdd3131348e76c3a2f31d2cd7cc11..f995fe637a385c1fa8f960f7d198b34a3639280f 100644 (file)
@@ -1,6 +1,6 @@
 import { Transaction } from 'sequelize/types'
 import { checkUrlsSameHost } from '@server/helpers/activitypub'
-import { deleteNonExistingModels } from '@server/helpers/database-utils'
+import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils'
 import { logger, LoggerTagsFn } from '@server/helpers/logger'
 import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail'
 import { setVideoTags } from '@server/lib/video'
@@ -111,8 +111,7 @@ export abstract class APVideoAbstractBuilder {
     const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
 
     // Remove video files that do not exist anymore
-    const destroyTasks = deleteNonExistingModels(video.VideoFiles || [], newVideoFiles, t)
-    await Promise.all(destroyTasks)
+    await deleteAllModels(filterNonExistingModels(video.VideoFiles || [], newVideoFiles), t)
 
     // Update or add other one
     const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
@@ -124,13 +123,11 @@ export abstract class APVideoAbstractBuilder {
     const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
 
     // Remove video playlists that do not exist anymore
-    const destroyTasks = deleteNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists, t)
-    await Promise.all(destroyTasks)
+    await deleteAllModels(filterNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists), t)
 
     video.VideoStreamingPlaylists = []
 
     for (const playlistAttributes of streamingPlaylistAttributes) {
-
       const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t)
       streamingPlaylistModel.Video = video
 
@@ -163,8 +160,7 @@ export abstract class APVideoAbstractBuilder {
 
     const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a))
 
-    const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t)
-    await Promise.all(destroyTasks)
+    await deleteAllModels(filterNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles), t)
 
     // Update or add other one
     const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
index 85548428c3d2e5d8bbafbafaa08fdbb26caf509b..1fa16295d4402a07871054de78db9c0a2a673858 100644 (file)
@@ -7,10 +7,11 @@ import { logger } from '@server/helpers/logger'
 import { getExtFromMimetype } from '@server/helpers/video'
 import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants'
 import { generateTorrentFileName } from '@server/lib/video-paths'
+import { VideoCaptionModel } from '@server/models/video/video-caption'
 import { VideoFileModel } from '@server/models/video/video-file'
 import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
 import { FilteredModelAttributes } from '@server/types'
-import { MChannelId, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models'
+import { isStreamingPlaylist, MChannelId, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models'
 import {
   ActivityHashTagObject,
   ActivityMagnetUrlObject,
@@ -23,7 +24,6 @@ import {
   VideoPrivacy,
   VideoStreamingPlaylistType
 } from '@shared/models'
-import { VideoCaptionModel } from '@server/models/video/video-caption'
 
 function getThumbnailFromIcons (videoObject: VideoObject) {
   let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
@@ -80,8 +80,8 @@ function getFileAttributesFromUrl (
 
     const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType)
     const resolution = fileUrl.height
-    const videoId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id
-    const videoStreamingPlaylistId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null
+    const videoId = isStreamingPlaylist(videoOrPlaylist) ? null : videoOrPlaylist.id
+    const videoStreamingPlaylistId = isStreamingPlaylist(videoOrPlaylist) ? videoOrPlaylist.id : null
 
     const attribute = {
       extname,
@@ -130,8 +130,13 @@ function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject:
 
     const attribute = {
       type: VideoStreamingPlaylistType.HLS,
+
+      playlistFilename: basename(playlistUrlObject.href),
       playlistUrl: playlistUrlObject.href,
+
+      segmentsSha256Filename: basename(segmentsSha256UrlObject.href),
       segmentsSha256Url: segmentsSha256UrlObject.href,
+
       p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
       p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
       videoId: video.id,
index 72194416dec8c9acb8825e90665642c3ff293b9e..a557c090f0207faed553627dc762007233a65708 100644 (file)
@@ -5,7 +5,7 @@ import validator from 'validator'
 import { escapeHTML } from '@shared/core-utils/renderer'
 import { HTMLServerConfig } from '@shared/models'
 import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
 import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos'
 import { isTestInstance, sha256 } from '../helpers/core-utils'
 import { logger } from '../helpers/logger'
@@ -162,7 +162,7 @@ class ClientHtml {
     let customHtml = ClientHtml.addTitleTag(html, escapeHTML(videoPlaylist.name))
     customHtml = ClientHtml.addDescriptionTag(customHtml, mdToPlainText(videoPlaylist.description))
 
-    const url = videoPlaylist.getWatchUrl()
+    const url = WEBSERVER.URL + videoPlaylist.getWatchStaticPath()
     const originUrl = videoPlaylist.url
     const title = escapeHTML(videoPlaylist.name)
     const siteName = escapeHTML(CONFIG.INSTANCE.NAME)
index 05be403f33a1dffba37248228a3d3a8143bc0a5e..32b02bc260f236e57098783f0712f93930a4d305 100644 (file)
@@ -1,7 +1,7 @@
 import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
 import { flatten, uniq } from 'lodash'
 import { basename, dirname, join } from 'path'
-import { MVideoWithFile } from '@server/types/models'
+import { MStreamingPlaylistFilesVideo, MVideoWithFile } from '@server/types/models'
 import { sha256 } from '../helpers/core-utils'
 import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils'
 import { logger } from '../helpers/logger'
@@ -12,7 +12,7 @@ import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from
 import { sequelizeTypescript } from '../initializers/database'
 import { VideoFileModel } from '../models/video/video-file'
 import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
-import { getVideoFilePath } from './video-paths'
+import { getHlsResolutionPlaylistFilename, getVideoFilePath } from './video-paths'
 
 async function updateStreamingPlaylistsInfohashesIfNeeded () {
   const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion()
@@ -22,25 +22,29 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () {
     await sequelizeTypescript.transaction(async t => {
       const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t)
 
-      playlist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlist.playlistUrl, videoFiles)
+      playlist.assignP2PMediaLoaderInfoHashes(playlist.Video, videoFiles)
       playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
+
       await playlist.save({ transaction: t })
     })
   }
 }
 
-async function updateMasterHLSPlaylist (video: MVideoWithFile) {
+async function updateMasterHLSPlaylist (video: MVideoWithFile, playlist: MStreamingPlaylistFilesVideo) {
   const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
+
   const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ]
-  const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
-  const streamingPlaylist = video.getHLSPlaylist()
 
-  for (const file of streamingPlaylist.VideoFiles) {
+  const masterPlaylistPath = join(directory, playlist.playlistFilename)
+
+  for (const file of playlist.VideoFiles) {
+    const playlistFilename = getHlsResolutionPlaylistFilename(file.filename)
+
     // If we did not generated a playlist for this resolution, skip
-    const filePlaylistPath = join(directory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
+    const filePlaylistPath = join(directory, playlistFilename)
     if (await pathExists(filePlaylistPath) === false) continue
 
-    const videoFilePath = getVideoFilePath(streamingPlaylist, file)
+    const videoFilePath = getVideoFilePath(playlist, file)
 
     const size = await getVideoStreamSize(videoFilePath)
 
@@ -58,29 +62,28 @@ async function updateMasterHLSPlaylist (video: MVideoWithFile) {
     line += `,CODECS="${codecs.filter(c => !!c).join(',')}"`
 
     masterPlaylists.push(line)
-    masterPlaylists.push(VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
+    masterPlaylists.push(playlistFilename)
   }
 
   await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n')
 }
 
-async function updateSha256VODSegments (video: MVideoWithFile) {
+async function updateSha256VODSegments (video: MVideoWithFile, playlist: MStreamingPlaylistFilesVideo) {
   const json: { [filename: string]: { [range: string]: string } } = {}
 
   const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
-  const hlsPlaylist = video.getHLSPlaylist()
 
   // For all the resolutions available for this video
-  for (const file of hlsPlaylist.VideoFiles) {
+  for (const file of playlist.VideoFiles) {
     const rangeHashes: { [range: string]: string } = {}
 
-    const videoPath = getVideoFilePath(hlsPlaylist, file)
-    const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
+    const videoPath = getVideoFilePath(playlist, file)
+    const resolutionPlaylistPath = join(playlistDirectory, getHlsResolutionPlaylistFilename(file.filename))
 
     // Maybe the playlist is not generated for this resolution yet
-    if (!await pathExists(playlistPath)) continue
+    if (!await pathExists(resolutionPlaylistPath)) continue
 
-    const playlistContent = await readFile(playlistPath)
+    const playlistContent = await readFile(resolutionPlaylistPath)
     const ranges = getRangesFromPlaylist(playlistContent.toString())
 
     const fd = await open(videoPath, 'r')
@@ -96,7 +99,7 @@ async function updateSha256VODSegments (video: MVideoWithFile) {
     json[videoFilename] = rangeHashes
   }
 
-  const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
+  const outputPath = join(playlistDirectory, playlist.segmentsSha256Filename)
   await outputJSON(outputPath, json)
 }
 
index 1caca1dcc04bb386d9c4efa7751bea6f85b964f9..56e2b0cebce3270933f948092fb82cf4015453ae 100644 (file)
@@ -12,7 +12,7 @@ import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants'
 import { VideoModel } from '@server/models/video/video'
 import { VideoCommentModel } from '@server/models/video/video-comment'
 import { VideoShareModel } from '@server/models/video/video-share'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
 
index 187cb652e0119a1b7a693b5f77b70ca21ccf3bda..4d199f24739bad97cf328020517fc8713edb2f9e 100644 (file)
@@ -2,7 +2,7 @@ import * as Bull from 'bull'
 import { copy, stat } from 'fs-extra'
 import { getLowercaseExtension } from '@server/helpers/core-utils'
 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
-import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
+import { generateWebTorrentVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 import { UserModel } from '@server/models/user/user'
 import { MVideoFullLight } from '@server/types/models'
 import { VideoFileImportPayload } from '@shared/models'
@@ -61,8 +61,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
 
   if (currentVideoFile) {
     // Remove old file and old torrent
-    await video.removeFile(currentVideoFile)
-    await currentVideoFile.removeTorrent()
+    await video.removeFileAndTorrent(currentVideoFile)
     // Remove the old video file from the array
     video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
 
@@ -72,7 +71,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
   const newVideoFile = new VideoFileModel({
     resolution: videoFileResolution,
     extname: fileExt,
-    filename: generateVideoFilename(video, false, videoFileResolution, fileExt),
+    filename: generateWebTorrentVideoFilename(videoFileResolution, fileExt),
     size,
     fps,
     videoId: video.id
index 55498003db1af2c7fc7ea6ee3680e701031d8e78..6e425d09c4bb9fbfd0e6a0227c977a9cfa64cf05 100644 (file)
@@ -8,7 +8,7 @@ import { Hooks } from '@server/lib/plugins/hooks'
 import { ServerConfigManager } from '@server/lib/server-config-manager'
 import { isAbleToUploadVideo } from '@server/lib/user'
 import { addOptimizeOrMergeAudioJob } from '@server/lib/video'
-import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
+import { generateWebTorrentVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 import { ThumbnailModel } from '@server/models/video/thumbnail'
 import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import'
 import {
@@ -124,7 +124,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
       extname: fileExt,
       resolution: videoFileResolution,
       size: stats.size,
-      filename: generateVideoFilename(videoImport.Video, false, videoFileResolution, fileExt),
+      filename: generateWebTorrentVideoFilename(videoFileResolution, fileExt),
       fps,
       videoId: videoImport.videoId
     }
index 9eba41bf8ca0a3c3ea287aac28a3fe63a9239720..386ccdc7b6d76e702140c63d25b4992ba1a70aa5 100644 (file)
@@ -7,12 +7,12 @@ import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server
 import { generateVideoMiniature } from '@server/lib/thumbnail'
 import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding'
 import { publishAndFederateIfNeeded } from '@server/lib/video'
-import { getHLSDirectory } from '@server/lib/video-paths'
+import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHLSDirectory } from '@server/lib/video-paths'
 import { VideoModel } from '@server/models/video/video'
 import { VideoFileModel } from '@server/models/video/video-file'
 import { VideoLiveModel } from '@server/models/video/video-live'
 import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
-import { MVideo, MVideoLive } from '@server/types/models'
+import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models'
 import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 
@@ -43,7 +43,7 @@ async function processVideoLiveEnding (job: Bull.Job) {
     return cleanupLive(video, streamingPlaylist)
   }
 
-  return saveLive(video, live)
+  return saveLive(video, live, streamingPlaylist)
 }
 
 // ---------------------------------------------------------------------------
@@ -54,14 +54,14 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function saveLive (video: MVideo, live: MVideoLive) {
+async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MStreamingPlaylist) {
   const hlsDirectory = getHLSDirectory(video, false)
   const replayDirectory = join(hlsDirectory, VIDEO_LIVE.REPLAY_DIRECTORY)
 
   const rootFiles = await readdir(hlsDirectory)
 
   const playlistFiles = rootFiles.filter(file => {
-    return file.endsWith('.m3u8') && file !== 'master.m3u8'
+    return file.endsWith('.m3u8') && file !== streamingPlaylist.playlistFilename
   })
 
   await cleanupLiveFiles(hlsDirectory)
@@ -80,7 +80,12 @@ async function saveLive (video: MVideo, live: MVideoLive) {
 
   const hlsPlaylist = videoWithFiles.getHLSPlaylist()
   await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id)
+
+  // Reset playlist
   hlsPlaylist.VideoFiles = []
+  hlsPlaylist.playlistFilename = generateHLSMasterPlaylistFilename()
+  hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename()
+  await hlsPlaylist.save()
 
   let durationDone = false
 
index f5ba6f435053bc41ab8bbf26a21d72a6abcd991f..36d9594af998f4e64103ef1093bdb12c5bedf9c6 100644 (file)
@@ -125,8 +125,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
   if (payload.isMaxQuality && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
     // Remove webtorrent files if not enabled
     for (const file of video.VideoFiles) {
-      await video.removeFile(file)
-      await file.removeTorrent()
+      await video.removeFileAndTorrent(file)
       await file.destroy()
     }
 
index 014cd3fcf12d38abf476a79fc3711e29fa8bbb0b..f106d69fb119180504e12114e15e64e791c3ce59 100644 (file)
@@ -4,24 +4,25 @@ import { isTestInstance } from '@server/helpers/core-utils'
 import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
 import { logger, loggerTagsFactory } from '@server/helpers/logger'
 import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
-import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants'
+import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME } from '@server/initializers/constants'
 import { UserModel } from '@server/models/user/user'
 import { VideoModel } from '@server/models/video/video'
 import { VideoLiveModel } from '@server/models/video/video-live'
 import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
-import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models'
+import { MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models'
 import { VideoState, VideoStreamingPlaylistType } from '@shared/models'
 import { federateVideoIfNeeded } from '../activitypub/videos'
 import { JobQueue } from '../job-queue'
 import { PeerTubeSocket } from '../peertube-socket'
+import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '../video-paths'
 import { LiveQuotaStore } from './live-quota-store'
 import { LiveSegmentShaStore } from './live-segment-sha-store'
 import { cleanupLive } from './live-utils'
 import { MuxingSession } from './shared'
 
-const NodeRtmpSession = require('node-media-server/node_rtmp_session')
-const context = require('node-media-server/node_core_ctx')
-const nodeMediaServerLogger = require('node-media-server/node_core_logger')
+const NodeRtmpSession = require('node-media-server/src/node_rtmp_session')
+const context = require('node-media-server/src/node_core_ctx')
+const nodeMediaServerLogger = require('node-media-server/src/node_core_logger')
 
 // Disable node media server logs
 nodeMediaServerLogger.setLogType(0)
@@ -392,19 +393,18 @@ class LiveManager {
     return resolutionsEnabled.concat([ originResolution ])
   }
 
-  private async createLivePlaylist (video: MVideo, allResolutions: number[]) {
-    const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
-    const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
-      videoId: video.id,
-      playlistUrl,
-      segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive),
-      p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, allResolutions),
-      p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
+  private async createLivePlaylist (video: MVideo, allResolutions: number[]): Promise<MStreamingPlaylistVideo> {
+    const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
 
-      type: VideoStreamingPlaylistType.HLS
-    }, { returning: true }) as [ MStreamingPlaylist, boolean ]
+    playlist.playlistFilename = generateHLSMasterPlaylistFilename(true)
+    playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(true)
 
-    return Object.assign(videoStreamingPlaylist, { Video: video })
+    playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
+    playlist.type = VideoStreamingPlaylistType.HLS
+
+    playlist.assignP2PMediaLoaderInfoHashes(video, allResolutions)
+
+    return playlist.save()
   }
 
   static get Instance () {
index 26467f0605face174f6fad83a8fdff8f829110ff..709d6c61549faa61d2cd87fedae97ea29da7454e 100644 (file)
@@ -112,13 +112,16 @@ class MuxingSession extends EventEmitter {
     this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED
       ? await getLiveTranscodingCommand({
         rtmpUrl: this.rtmpUrl,
+
         outPath,
+        masterPlaylistName: this.streamingPlaylist.playlistFilename,
+
         resolutions: this.allResolutions,
         fps: this.fps,
         availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
         profile: CONFIG.LIVE.TRANSCODING.PROFILE
       })
-      : getLiveMuxingCommand(this.rtmpUrl, outPath)
+      : getLiveMuxingCommand(this.rtmpUrl, outPath, this.streamingPlaylist.playlistFilename)
 
     logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags)
 
@@ -182,7 +185,7 @@ class MuxingSession extends EventEmitter {
   }
 
   private watchMasterFile (outPath: string) {
-    this.masterWatcher = chokidar.watch(outPath + '/master.m3u8')
+    this.masterWatcher = chokidar.watch(outPath + '/' + this.streamingPlaylist.playlistFilename)
 
     this.masterWatcher.on('add', async () => {
       this.emit('master-playlist-created', { videoId: this.videoId })
index 14e00518e5a133ba19c8e6f9b1c242a4f9bc1e70..a42ab5b7ff66acbace7494c664a3f4c38c1c841f 100644 (file)
@@ -23,7 +23,7 @@ import { ActivityCreate } from '../../shared/models/activitypub'
 import { VideoObject } from '../../shared/models/activitypub/objects'
 import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
 import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos'
-import { VideoCommentCreate } from '../../shared/models/videos/comment/video-comment.model'
+import { VideoCommentCreate } from '../../shared/models/videos/comment'
 import { ActorModel } from '../models/actor/actor'
 import { UserModel } from '../models/user/user'
 import { VideoModel } from '../models/video/video'
index 09275f9ba8ea6e59a268bc9b2a5243931e8f980b..af533effd8100700098235afd48c5f11304d9dbd 100644 (file)
@@ -1,13 +1,7 @@
 import * as express from 'express'
 import { logger } from '@server/helpers/logger'
-import {
-  VIDEO_CATEGORIES,
-  VIDEO_LANGUAGES,
-  VIDEO_LICENCES,
-  VIDEO_PLAYLIST_PRIVACIES,
-  VIDEO_PRIVACIES
-} from '@server/initializers/constants'
 import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
+import { VideoConstantManagerFactory } from '@server/lib/plugins/video-constant-manager-factory'
 import { PluginModel } from '@server/models/server/plugin'
 import {
   RegisterServerAuthExternalOptions,
@@ -18,41 +12,18 @@ import {
 } from '@server/types/plugins'
 import {
   EncoderOptionsBuilder,
-  PluginPlaylistPrivacyManager,
   PluginSettingsManager,
   PluginStorageManager,
-  PluginVideoCategoryManager,
-  PluginVideoLanguageManager,
-  PluginVideoLicenceManager,
-  PluginVideoPrivacyManager,
   RegisterServerHookOptions,
   RegisterServerSettingOptions,
-  serverHookObject
+  serverHookObject,
+  VideoPlaylistPrivacy,
+  VideoPrivacy
 } from '@shared/models'
 import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles'
 import { buildPluginHelpers } from './plugin-helpers-builder'
 
-type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
-type VideoConstant = { [key in number | string]: string }
-
-type UpdatedVideoConstant = {
-  [name in AlterableVideoConstant]: {
-    [ npmName: string]: {
-      added: { key: number | string, label: string }[]
-      deleted: { key: number | string, label: string }[]
-    }
-  }
-}
-
 export class RegisterHelpers {
-  private readonly updatedVideoConstants: UpdatedVideoConstant = {
-    playlistPrivacy: { },
-    privacy: { },
-    language: { },
-    licence: { },
-    category: { }
-  }
-
   private readonly transcodingProfiles: {
     [ npmName: string ]: {
       type: 'vod' | 'live'
@@ -78,6 +49,7 @@ export class RegisterHelpers {
   private readonly onSettingsChangeCallbacks: ((settings: any) => Promise<any>)[] = []
 
   private readonly router: express.Router
+  private readonly videoConstantManagerFactory: VideoConstantManagerFactory
 
   constructor (
     private readonly npmName: string,
@@ -85,6 +57,7 @@ export class RegisterHelpers {
     private readonly onHookAdded: (options: RegisterServerHookOptions) => void
   ) {
     this.router = express.Router()
+    this.videoConstantManagerFactory = new VideoConstantManagerFactory(this.npmName)
   }
 
   buildRegisterHelpers (): RegisterServerOptions {
@@ -96,13 +69,13 @@ export class RegisterHelpers {
     const settingsManager = this.buildSettingsManager()
     const storageManager = this.buildStorageManager()
 
-    const videoLanguageManager = this.buildVideoLanguageManager()
+    const videoLanguageManager = this.videoConstantManagerFactory.createVideoConstantManager<string>('language')
 
-    const videoLicenceManager = this.buildVideoLicenceManager()
-    const videoCategoryManager = this.buildVideoCategoryManager()
+    const videoLicenceManager = this.videoConstantManagerFactory.createVideoConstantManager<number>('licence')
+    const videoCategoryManager = this.videoConstantManagerFactory.createVideoConstantManager<number>('category')
 
-    const videoPrivacyManager = this.buildVideoPrivacyManager()
-    const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
+    const videoPrivacyManager = this.videoConstantManagerFactory.createVideoConstantManager<VideoPrivacy>('privacy')
+    const playlistPrivacyManager = this.videoConstantManagerFactory.createVideoConstantManager<VideoPlaylistPrivacy>('playlistPrivacy')
 
     const transcodingManager = this.buildTranscodingManager()
 
@@ -122,12 +95,38 @@ export class RegisterHelpers {
       settingsManager,
       storageManager,
 
-      videoLanguageManager,
-      videoCategoryManager,
-      videoLicenceManager,
+      videoLanguageManager: {
+        ...videoLanguageManager,
+        /** @deprecated use `addConstant` instead **/
+        addLanguage: videoLanguageManager.addConstant,
+        /** @deprecated use `deleteConstant` instead **/
+        deleteLanguage: videoLanguageManager.deleteConstant
+      },
+      videoCategoryManager: {
+        ...videoCategoryManager,
+        /** @deprecated use `addConstant` instead **/
+        addCategory: videoCategoryManager.addConstant,
+        /** @deprecated use `deleteConstant` instead **/
+        deleteCategory: videoCategoryManager.deleteConstant
+      },
+      videoLicenceManager: {
+        ...videoLicenceManager,
+        /** @deprecated use `addConstant` instead **/
+        addLicence: videoLicenceManager.addConstant,
+        /** @deprecated use `deleteConstant` instead **/
+        deleteLicence: videoLicenceManager.deleteConstant
+      },
 
-      videoPrivacyManager,
-      playlistPrivacyManager,
+      videoPrivacyManager: {
+        ...videoPrivacyManager,
+        /** @deprecated use `deleteConstant` instead **/
+        deletePrivacy: videoPrivacyManager.deleteConstant
+      },
+      playlistPrivacyManager: {
+        ...playlistPrivacyManager,
+        /** @deprecated use `deleteConstant` instead **/
+        deletePlaylistPrivacy: playlistPrivacyManager.deleteConstant
+      },
 
       transcodingManager,
 
@@ -141,29 +140,7 @@ export class RegisterHelpers {
   }
 
   reinitVideoConstants (npmName: string) {
-    const hash = {
-      language: VIDEO_LANGUAGES,
-      licence: VIDEO_LICENCES,
-      category: VIDEO_CATEGORIES,
-      privacy: VIDEO_PRIVACIES,
-      playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES
-    }
-    const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ]
-
-    for (const type of types) {
-      const updatedConstants = this.updatedVideoConstants[type][npmName]
-      if (!updatedConstants) continue
-
-      for (const added of updatedConstants.added) {
-        delete hash[type][added.key]
-      }
-
-      for (const deleted of updatedConstants.deleted) {
-        hash[type][deleted.key] = deleted.label
-      }
-
-      delete this.updatedVideoConstants[type][npmName]
-    }
+    this.videoConstantManagerFactory.resetVideoConstants(npmName)
   }
 
   reinitTranscodingProfilesAndEncoders (npmName: string) {
@@ -291,119 +268,6 @@ export class RegisterHelpers {
     }
   }
 
-  private buildVideoLanguageManager (): PluginVideoLanguageManager {
-    return {
-      addLanguage: (key: string, label: string) => {
-        return this.addConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label })
-      },
-
-      deleteLanguage: (key: string) => {
-        return this.deleteConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
-      }
-    }
-  }
-
-  private buildVideoCategoryManager (): PluginVideoCategoryManager {
-    return {
-      addCategory: (key: number, label: string) => {
-        return this.addConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label })
-      },
-
-      deleteCategory: (key: number) => {
-        return this.deleteConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
-      }
-    }
-  }
-
-  private buildVideoPrivacyManager (): PluginVideoPrivacyManager {
-    return {
-      deletePrivacy: (key: number) => {
-        return this.deleteConstant({ npmName: this.npmName, type: 'privacy', obj: VIDEO_PRIVACIES, key })
-      }
-    }
-  }
-
-  private buildPlaylistPrivacyManager (): PluginPlaylistPrivacyManager {
-    return {
-      deletePlaylistPrivacy: (key: number) => {
-        return this.deleteConstant({ npmName: this.npmName, type: 'playlistPrivacy', obj: VIDEO_PLAYLIST_PRIVACIES, key })
-      }
-    }
-  }
-
-  private buildVideoLicenceManager (): PluginVideoLicenceManager {
-    return {
-      addLicence: (key: number, label: string) => {
-        return this.addConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key, label })
-      },
-
-      deleteLicence: (key: number) => {
-        return this.deleteConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key })
-      }
-    }
-  }
-
-  private addConstant<T extends string | number> (parameters: {
-    npmName: string
-    type: AlterableVideoConstant
-    obj: VideoConstant
-    key: T
-    label: string
-  }) {
-    const { npmName, type, obj, key, label } = parameters
-
-    if (obj[key]) {
-      logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
-      return false
-    }
-
-    if (!this.updatedVideoConstants[type][npmName]) {
-      this.updatedVideoConstants[type][npmName] = {
-        added: [],
-        deleted: []
-      }
-    }
-
-    this.updatedVideoConstants[type][npmName].added.push({ key, label })
-    obj[key] = label
-
-    return true
-  }
-
-  private deleteConstant<T extends string | number> (parameters: {
-    npmName: string
-    type: AlterableVideoConstant
-    obj: VideoConstant
-    key: T
-  }) {
-    const { npmName, type, obj, key } = parameters
-
-    if (!obj[key]) {
-      logger.warn('Cannot delete %s by plugin %s: key %s does not exist.', type, npmName, key)
-      return false
-    }
-
-    if (!this.updatedVideoConstants[type][npmName]) {
-      this.updatedVideoConstants[type][npmName] = {
-        added: [],
-        deleted: []
-      }
-    }
-
-    const updatedConstants = this.updatedVideoConstants[type][npmName]
-
-    const alreadyAdded = updatedConstants.added.find(a => a.key === key)
-    if (alreadyAdded) {
-      updatedConstants.added.filter(a => a.key !== key)
-    } else if (obj[key]) {
-      updatedConstants.deleted.push({ key, label: obj[key] })
-    }
-
-    delete obj[key]
-
-    return true
-  }
-
   private buildTranscodingManager () {
     const self = this
 
diff --git a/server/lib/plugins/video-constant-manager-factory.ts b/server/lib/plugins/video-constant-manager-factory.ts
new file mode 100644 (file)
index 0000000..f04dde2
--- /dev/null
@@ -0,0 +1,139 @@
+import { logger } from '@server/helpers/logger'
+import {
+  VIDEO_CATEGORIES,
+  VIDEO_LANGUAGES,
+  VIDEO_LICENCES,
+  VIDEO_PLAYLIST_PRIVACIES,
+  VIDEO_PRIVACIES
+} from '@server/initializers/constants'
+import { ConstantManager } from '@shared/models/plugins/server/plugin-constant-manager.model'
+
+type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
+type VideoConstant = Record<number | string, string>
+
+type UpdatedVideoConstant = {
+  [name in AlterableVideoConstant]: {
+    [ npmName: string]: {
+      added: VideoConstant[]
+      deleted: VideoConstant[]
+    }
+  }
+}
+
+const constantsHash: { [key in AlterableVideoConstant]: VideoConstant } = {
+  language: VIDEO_LANGUAGES,
+  licence: VIDEO_LICENCES,
+  category: VIDEO_CATEGORIES,
+  privacy: VIDEO_PRIVACIES,
+  playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES
+}
+
+export class VideoConstantManagerFactory {
+  private readonly updatedVideoConstants: UpdatedVideoConstant = {
+    playlistPrivacy: { },
+    privacy: { },
+    language: { },
+    licence: { },
+    category: { }
+  }
+
+  constructor (
+    private readonly npmName: string
+  ) {}
+
+  public resetVideoConstants (npmName: string) {
+    const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ]
+    for (const type of types) {
+      this.resetConstants({ npmName, type })
+    }
+  }
+
+  private resetConstants (parameters: { npmName: string, type: AlterableVideoConstant }) {
+    const { npmName, type } = parameters
+    const updatedConstants = this.updatedVideoConstants[type][npmName]
+
+    if (!updatedConstants) return
+
+    for (const added of updatedConstants.added) {
+      delete constantsHash[type][added.key]
+    }
+
+    for (const deleted of updatedConstants.deleted) {
+      constantsHash[type][deleted.key] = deleted.label
+    }
+
+    delete this.updatedVideoConstants[type][npmName]
+  }
+
+  public createVideoConstantManager<K extends number | string>(type: AlterableVideoConstant): ConstantManager<K> {
+    const { npmName } = this
+    return {
+      addConstant: (key: K, label: string) => this.addConstant({ npmName, type, key, label }),
+      deleteConstant: (key: K) => this.deleteConstant({ npmName, type, key }),
+      getConstantValue: (key: K) => constantsHash[type][key],
+      getConstants: () => constantsHash[type] as Record<K, string>,
+      resetConstants: () => this.resetConstants({ npmName, type })
+    }
+  }
+
+  private addConstant<T extends string | number> (parameters: {
+    npmName: string
+    type: AlterableVideoConstant
+    key: T
+    label: string
+  }) {
+    const { npmName, type, key, label } = parameters
+    const obj = constantsHash[type]
+
+    if (obj[key]) {
+      logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
+      return false
+    }
+
+    if (!this.updatedVideoConstants[type][npmName]) {
+      this.updatedVideoConstants[type][npmName] = {
+        added: [],
+        deleted: []
+      }
+    }
+
+    this.updatedVideoConstants[type][npmName].added.push({ key: key, label } as VideoConstant)
+    obj[key] = label
+
+    return true
+  }
+
+  private deleteConstant<T extends string | number> (parameters: {
+    npmName: string
+    type: AlterableVideoConstant
+    key: T
+  }) {
+    const { npmName, type, key } = parameters
+    const obj = constantsHash[type]
+
+    if (!obj[key]) {
+      logger.warn('Cannot delete %s by plugin %s: key %s does not exist.', type, npmName, key)
+      return false
+    }
+
+    if (!this.updatedVideoConstants[type][npmName]) {
+      this.updatedVideoConstants[type][npmName] = {
+        added: [],
+        deleted: []
+      }
+    }
+
+    const updatedConstants = this.updatedVideoConstants[type][npmName]
+
+    const alreadyAdded = updatedConstants.added.find(a => a.key === key)
+    if (alreadyAdded) {
+      updatedConstants.added.filter(a => a.key !== key)
+    } else if (obj[key]) {
+      updatedConstants.deleted.push({ key, label: obj[key] } as VideoConstant)
+    }
+
+    delete obj[key]
+
+    return true
+  }
+}
index 9a1ae3ec50861dc9e26695dd6ca6680e7f9dc0b8..c95e109b03cf8460b259236ac19986535c46d0ff 100644 (file)
@@ -1,12 +1,12 @@
+import { chunk } from 'lodash'
+import { compareSemVer } from '@shared/core-utils'
 import { logger } from '../../helpers/logger'
-import { AbstractScheduler } from './abstract-scheduler'
-import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
 import { CONFIG } from '../../initializers/config'
+import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
 import { PluginModel } from '../../models/server/plugin'
-import { chunk } from 'lodash'
-import { getLatestPluginsVersion } from '../plugins/plugin-index'
-import { compareSemVer } from '../../../shared/core-utils/miscs/miscs'
 import { Notifier } from '../notifier'
+import { getLatestPluginsVersion } from '../plugins/plugin-index'
+import { AbstractScheduler } from './abstract-scheduler'
 
 export class PluginsCheckScheduler extends AbstractScheduler {
 
index b5a5eb697400e178879b4a1a7db3e4f014634c07..103ab1fab676f5f2225bd6d15bbe05bb1e708a08 100644 (file)
@@ -267,7 +267,8 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
     logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy)
 
     const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid)
-    await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT)
+    const masterPlaylistUrl = playlist.getMasterPlaylistUrl(video)
+    await downloadPlaylistSegments(masterPlaylistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT)
 
     const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({
       expiresOn,
@@ -282,7 +283,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
 
     await sendCreateCacheFile(serverActor, video, createdModel)
 
-    logger.info('Duplicated playlist %s -> %s.', playlist.playlistUrl, createdModel.url)
+    logger.info('Duplicated playlist %s -> %s.', masterPlaylistUrl, createdModel.url)
   }
 
   private async extendsExpirationOf (redundancy: MVideoRedundancyVideo, expiresAfterMs: number) {
@@ -330,7 +331,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
   private buildEntryLogId (object: MVideoRedundancyFileVideo | MVideoRedundancyStreamingPlaylistVideo) {
     if (isMVideoRedundancyFileVideo(object)) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}`
 
-    return `${object.VideoStreamingPlaylist.playlistUrl}`
+    return `${object.VideoStreamingPlaylist.getMasterPlaylistUrl(object.VideoStreamingPlaylist.Video)}`
   }
 
   private getTotalFileSizes (files: MVideoFile[], playlists: MStreamingPlaylistFiles[]) {
index 1ad63baf3a88a53478e133f7169865cc488abb53..d2a556360afbdc0ca525f654c6b97bb45d6d2466 100644 (file)
@@ -10,11 +10,18 @@ import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers
 import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils'
 import { logger } from '../../helpers/logger'
 import { CONFIG } from '../../initializers/config'
-import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../../initializers/constants'
+import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants'
 import { VideoFileModel } from '../../models/video/video-file'
 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
 import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls'
-import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from '../video-paths'
+import {
+  generateHLSMasterPlaylistFilename,
+  generateHlsSha256SegmentsFilename,
+  generateHLSVideoFilename,
+  generateWebTorrentVideoFilename,
+  getHlsResolutionPlaylistFilename,
+  getVideoFilePath
+} from '../video-paths'
 import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
 
 /**
@@ -60,7 +67,7 @@ async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile
 
     // Important to do this before getVideoFilename() to take in account the new filename
     inputVideoFile.extname = newExtname
-    inputVideoFile.filename = generateVideoFilename(video, false, resolution, newExtname)
+    inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
 
     const videoOutputPath = getVideoFilePath(video, inputVideoFile)
 
@@ -86,7 +93,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolut
   const newVideoFile = new VideoFileModel({
     resolution,
     extname,
-    filename: generateVideoFilename(video, false, resolution, extname),
+    filename: generateWebTorrentVideoFilename(resolution, extname),
     size: 0,
     videoId: video.id
   })
@@ -169,7 +176,7 @@ async function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoRes
 
   // Important to do this before getVideoFilename() to take in account the new file extension
   inputVideoFile.extname = newExtname
-  inputVideoFile.filename = generateVideoFilename(video, false, inputVideoFile.resolution, newExtname)
+  inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
 
   const videoOutputPath = getVideoFilePath(video, inputVideoFile)
   // ffmpeg generated a new video file, so update the video duration
@@ -271,15 +278,15 @@ async function generateHlsPlaylistCommon (options: {
   const videoTranscodedBasePath = join(transcodeDirectory, type)
   await ensureDir(videoTranscodedBasePath)
 
-  const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution)
-  const playlistFilename = VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)
-  const playlistFileTranscodePath = join(videoTranscodedBasePath, playlistFilename)
+  const videoFilename = generateHLSVideoFilename(resolution)
+  const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
+  const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
 
   const transcodeOptions = {
     type,
 
     inputPath,
-    outputPath: playlistFileTranscodePath,
+    outputPath: resolutionPlaylistFileTranscodePath,
 
     availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
     profile: CONFIG.TRANSCODING.PROFILE,
@@ -299,19 +306,23 @@ async function generateHlsPlaylistCommon (options: {
 
   await transcode(transcodeOptions)
 
-  const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
-
   // Create or update the playlist
-  const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
-    videoId: video.id,
-    playlistUrl,
-    segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive),
-    p2pMediaLoaderInfohashes: [],
-    p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
+  const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
+
+  if (!playlist.playlistFilename) {
+    playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
+  }
+
+  if (!playlist.segmentsSha256Filename) {
+    playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
+  }
+
+  playlist.p2pMediaLoaderInfohashes = []
+  playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
 
-    type: VideoStreamingPlaylistType.HLS
-  }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ]
-  videoStreamingPlaylist.Video = video
+  playlist.type = VideoStreamingPlaylistType.HLS
+
+  await playlist.save()
 
   // Build the new playlist file
   const extname = extnameUtil(videoFilename)
@@ -319,20 +330,20 @@ async function generateHlsPlaylistCommon (options: {
     resolution,
     extname,
     size: 0,
-    filename: generateVideoFilename(video, true, resolution, extname),
+    filename: videoFilename,
     fps: -1,
-    videoStreamingPlaylistId: videoStreamingPlaylist.id
+    videoStreamingPlaylistId: playlist.id
   })
 
-  const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile)
+  const videoFilePath = getVideoFilePath(playlist, newVideoFile)
 
   // Move files from tmp transcoded directory to the appropriate place
   const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
   await ensureDir(baseHlsDirectory)
 
   // Move playlist file
-  const playlistPath = join(baseHlsDirectory, playlistFilename)
-  await move(playlistFileTranscodePath, playlistPath, { overwrite: true })
+  const resolutionPlaylistPath = join(baseHlsDirectory, resolutionPlaylistFilename)
+  await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
   // Move video file
   await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
 
@@ -342,20 +353,20 @@ async function generateHlsPlaylistCommon (options: {
   newVideoFile.fps = await getVideoFileFPS(videoFilePath)
   newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
 
-  await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
+  await createTorrentAndSetInfoHash(playlist, newVideoFile)
 
   await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
-  videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
 
-  videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(
-    playlistUrl, videoStreamingPlaylist.VideoFiles
-  )
-  await videoStreamingPlaylist.save()
+  const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
+  playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
+  playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
+
+  await playlist.save()
 
-  video.setHLSPlaylist(videoStreamingPlaylist)
+  video.setHLSPlaylist(playlist)
 
-  await updateMasterHLSPlaylist(video)
-  await updateSha256VODSegments(video)
+  await updateMasterHLSPlaylist(video, playlistWithFiles)
+  await updateSha256VODSegments(video, playlistWithFiles)
 
-  return playlistPath
+  return resolutionPlaylistPath
 }
index 1708c479a98d58b654a937098456abb857be3640..1e43821083a9c07907c07e3192a63874e5727a41 100644 (file)
@@ -3,29 +3,17 @@ import { extractVideo } from '@server/helpers/video'
 import { CONFIG } from '@server/initializers/config'
 import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_PATHS, WEBSERVER } from '@server/initializers/constants'
 import { isStreamingPlaylist, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models'
+import { buildUUID } from '@server/helpers/uuid'
+import { removeFragmentedMP4Ext } from '@shared/core-utils'
 
 // ################## Video file name ##################
 
-function generateVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, isHls: boolean, resolution: number, extname: string) {
-  const video = extractVideo(videoOrPlaylist)
-
-  // FIXME: use a generated uuid instead, that will break compatibility with PeerTube < 3.1
-  // const uuid = uuidv4()
-  const uuid = video.uuid
-
-  if (isHls) {
-    return generateVideoStreamingPlaylistName(uuid, resolution)
-  }
-
-  return generateWebTorrentVideoName(uuid, resolution, extname)
+function generateWebTorrentVideoFilename (resolution: number, extname: string) {
+  return buildUUID() + '-' + resolution + extname
 }
 
-function generateVideoStreamingPlaylistName (uuid: string, resolution: number) {
-  return `${uuid}-${resolution}-fragmented.mp4`
-}
-
-function generateWebTorrentVideoName (uuid: string, resolution: number, extname: string) {
-  return uuid + '-' + resolution + extname
+function generateHLSVideoFilename (resolution: number) {
+  return `${buildUUID()}-${resolution}-fragmented.mp4`
 }
 
 function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) {
@@ -63,15 +51,28 @@ function getHLSDirectory (video: MVideoUUID, isRedundancy = false) {
   return join(baseDir, video.uuid)
 }
 
+function getHlsResolutionPlaylistFilename (videoFilename: string) {
+  // Video file name already contain resolution
+  return removeFragmentedMP4Ext(videoFilename) + '.m3u8'
+}
+
+function generateHLSMasterPlaylistFilename (isLive = false) {
+  if (isLive) return 'master.m3u8'
+
+  return buildUUID() + '-master.m3u8'
+}
+
+function generateHlsSha256SegmentsFilename (isLive = false) {
+  if (isLive) return 'segments-sha256.json'
+
+  return buildUUID() + '-segments-sha256.json'
+}
+
 // ################## Torrents ##################
 
 function generateTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, resolution: number) {
-  const video = extractVideo(videoOrPlaylist)
   const extension = '.torrent'
-
-  // FIXME: use a generated uuid instead, that will break compatibility with PeerTube < 3.1
-  // const uuid = uuidv4()
-  const uuid = video.uuid
+  const uuid = buildUUID()
 
   if (isStreamingPlaylist(videoOrPlaylist)) {
     return `${uuid}-${resolution}-${videoOrPlaylist.getStringType()}${extension}`
@@ -95,15 +96,18 @@ function getLocalVideoFileMetadataUrl (video: MVideoUUID, videoFile: MVideoFile)
 // ---------------------------------------------------------------------------
 
 export {
-  generateVideoStreamingPlaylistName,
-  generateWebTorrentVideoName,
-  generateVideoFilename,
+  generateHLSVideoFilename,
+  generateWebTorrentVideoFilename,
+
   getVideoFilePath,
 
   generateTorrentFileName,
   getTorrentFilePath,
 
   getHLSDirectory,
+  generateHLSMasterPlaylistFilename,
+  generateHlsSha256SegmentsFilename,
+  getHlsResolutionPlaylistFilename,
 
   getLocalVideoFileMetadataUrl,
 
index daf998704b55cc2eae78fec787f15c019a993ec9..61fee4949291ec3786f5da203c44528f82e26869 100644 (file)
@@ -5,7 +5,7 @@ import { sequelizeTypescript } from '@server/initializers/database'
 import { TagModel } from '@server/models/video/tag'
 import { VideoModel } from '@server/models/video/video'
 import { FilteredModelAttributes } from '@server/types'
-import { MThumbnail, MUserId, MVideo, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models'
+import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models'
 import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models'
 import { federateVideoIfNeeded } from './activitypub/videos'
 import { JobQueue } from './job-queue/job-queue'
@@ -105,7 +105,7 @@ async function publishAndFederateIfNeeded (video: MVideoUUID, wasLive = false) {
   }
 }
 
-async function addOptimizeOrMergeAudioJob (video: MVideo, videoFile: MVideoFile, user: MUserId) {
+async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoFile, user: MUserId) {
   let dataInput: VideoTranscodingPayload
 
   if (videoFile.isAudio()) {
index 6b43b7764b63daa8e4bdac8ca292f604e9c697e1..6ef90b2757091a66587d846872a2703357f75319 100644 (file)
@@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from 'express'
 import { getAPId } from '@server/helpers/activitypub'
 import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor'
 import { ActivityDelete, ActivityPubSignature } from '../../shared'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
 import { logger } from '../helpers/logger'
 import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto'
 import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants'
index 176461cc2cae41fb09aff44472adbb23080c2db7..9e6327b2379ed2c018803fc7567e91380dca8694 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { Socket } from 'socket.io'
 import { getAccessToken } from '@server/lib/auth/oauth-model'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
 import { logger } from '../helpers/logger'
 import { handleOAuthAuthenticate } from '../lib/auth/oauth'
 
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts
deleted file mode 100644 (file)
index 0708ee8..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Redis } from '../lib/redis'
-import * as apicache from 'apicache'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
-
-// Ensure Redis is initialized
-Redis.Instance.init()
-
-const defaultOptions = {
-  redisClient: Redis.Instance.getClient(),
-  appendKey: () => Redis.Instance.getPrefix(),
-  statusCodes: {
-    exclude: [
-      HttpStatusCode.FORBIDDEN_403,
-      HttpStatusCode.NOT_FOUND_404
-    ]
-  }
-}
-
-const cacheRoute = (extraOptions = {}) => apicache.options({
-  ...defaultOptions,
-  ...extraOptions
-}).middleware
-
-// ---------------------------------------------------------------------------
-
-export {
-  cacheRoute
-}
diff --git a/server/middlewares/cache/cache.ts b/server/middlewares/cache/cache.ts
new file mode 100644 (file)
index 0000000..48162a0
--- /dev/null
@@ -0,0 +1,32 @@
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
+import { Redis } from '../../lib/redis'
+import { ApiCache, APICacheOptions } from './shared'
+
+// Ensure Redis is initialized
+Redis.Instance.init()
+
+const defaultOptions: APICacheOptions = {
+  excludeStatus: [
+    HttpStatusCode.FORBIDDEN_403,
+    HttpStatusCode.NOT_FOUND_404
+  ]
+}
+
+function cacheRoute (duration: string) {
+  const instance = new ApiCache(defaultOptions)
+
+  return instance.buildMiddleware(duration)
+}
+
+function cacheRouteFactory (options: APICacheOptions) {
+  const instance = new ApiCache({ ...defaultOptions, ...options })
+
+  return instance.buildMiddleware.bind(instance)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  cacheRoute,
+  cacheRouteFactory
+}
diff --git a/server/middlewares/cache/index.ts b/server/middlewares/cache/index.ts
new file mode 100644 (file)
index 0000000..79b5128
--- /dev/null
@@ -0,0 +1 @@
+export * from './cache'
diff --git a/server/middlewares/cache/shared/api-cache.ts b/server/middlewares/cache/shared/api-cache.ts
new file mode 100644 (file)
index 0000000..f9f7b1b
--- /dev/null
@@ -0,0 +1,269 @@
+// Thanks: https://github.com/kwhitley/apicache
+// We duplicated the library because it is unmaintened and prevent us to upgrade to recent NodeJS versions
+
+import * as express from 'express'
+import { OutgoingHttpHeaders } from 'http'
+import { isTestInstance, parseDurationToMs } from '@server/helpers/core-utils'
+import { logger } from '@server/helpers/logger'
+import { Redis } from '@server/lib/redis'
+import { HttpStatusCode } from '@shared/models'
+
+export interface APICacheOptions {
+  headerBlacklist?: string[]
+  excludeStatus?: HttpStatusCode[]
+}
+
+interface CacheObject {
+  status: number
+  headers: OutgoingHttpHeaders
+  data: any
+  encoding: BufferEncoding
+  timestamp: number
+}
+
+export class ApiCache {
+
+  private readonly options: APICacheOptions
+  private readonly timers: { [ id: string ]: NodeJS.Timeout } = {}
+
+  private index: { all: string[] } = { all: [] }
+
+  constructor (options: APICacheOptions) {
+    this.options = {
+      headerBlacklist: [],
+      excludeStatus: [],
+
+      ...options
+    }
+  }
+
+  buildMiddleware (strDuration: string) {
+    const duration = parseDurationToMs(strDuration)
+
+    return (req: express.Request, res: express.Response, next: express.NextFunction) => {
+      const key = Redis.Instance.getPrefix() + 'api-cache-' + req.originalUrl
+      const redis = Redis.Instance.getClient()
+
+      if (!redis.connected) return this.makeResponseCacheable(res, next, key, duration)
+
+      try {
+        redis.hgetall(key, (err, obj) => {
+          if (!err && obj && obj.response) {
+            return this.sendCachedResponse(req, res, JSON.parse(obj.response), duration)
+          }
+
+          return this.makeResponseCacheable(res, next, key, duration)
+        })
+      } catch (err) {
+        return this.makeResponseCacheable(res, next, key, duration)
+      }
+    }
+  }
+
+  private shouldCacheResponse (response: express.Response) {
+    if (!response) return false
+    if (this.options.excludeStatus.includes(response.statusCode)) return false
+
+    return true
+  }
+
+  private addIndexEntries (key: string) {
+    this.index.all.unshift(key)
+  }
+
+  private filterBlacklistedHeaders (headers: OutgoingHttpHeaders) {
+    return Object.keys(headers)
+      .filter(key => !this.options.headerBlacklist.includes(key))
+      .reduce((acc, header) => {
+        acc[header] = headers[header]
+
+        return acc
+      }, {})
+  }
+
+  private createCacheObject (status: number, headers: OutgoingHttpHeaders, data: any, encoding: BufferEncoding) {
+    return {
+      status,
+      headers: this.filterBlacklistedHeaders(headers),
+      data,
+      encoding,
+
+      // Seconds since epoch, used to properly decrement max-age headers in cached responses.
+      timestamp: new Date().getTime() / 1000
+    } as CacheObject
+  }
+
+  private cacheResponse (key: string, value: object, duration: number) {
+    const redis = Redis.Instance.getClient()
+
+    if (redis.connected) {
+      try {
+        redis.hset(key, 'response', JSON.stringify(value))
+        redis.hset(key, 'duration', duration + '')
+        redis.expire(key, duration / 1000)
+      } catch (err) {
+        logger.error('Cannot set cache in redis.', { err })
+      }
+    }
+
+    // add automatic cache clearing from duration, includes max limit on setTimeout
+    this.timers[key] = setTimeout(() => this.clear(key), Math.min(duration, 2147483647))
+  }
+
+  private accumulateContent (res: express.Response, content: any) {
+    if (!content) return
+
+    if (typeof content === 'string') {
+      res.locals.apicache.content = (res.locals.apicache.content || '') + content
+      return
+    }
+
+    if (Buffer.isBuffer(content)) {
+      let oldContent = res.locals.apicache.content
+
+      if (typeof oldContent === 'string') {
+        oldContent = Buffer.from(oldContent)
+      }
+
+      if (!oldContent) {
+        oldContent = Buffer.alloc(0)
+      }
+
+      res.locals.apicache.content = Buffer.concat(
+        [ oldContent, content ],
+        oldContent.length + content.length
+      )
+
+      return
+    }
+
+    res.locals.apicache.content = content
+  }
+
+  private makeResponseCacheable (res: express.Response, next: express.NextFunction, key: string, duration: number) {
+    const self = this
+
+    res.locals.apicache = {
+      write: res.write,
+      writeHead: res.writeHead,
+      end: res.end,
+      cacheable: true,
+      content: undefined,
+      headers: {}
+    }
+
+    // Patch express
+    res.writeHead = function () {
+      if (self.shouldCacheResponse(res)) {
+        res.setHeader('cache-control', 'max-age=' + (duration / 1000).toFixed(0))
+      } else {
+        res.setHeader('cache-control', 'no-cache, no-store, must-revalidate')
+      }
+
+      res.locals.apicache.headers = Object.assign({}, res.getHeaders())
+      return res.locals.apicache.writeHead.apply(this, arguments as any)
+    }
+
+    res.write = function (chunk: any) {
+      self.accumulateContent(res, chunk)
+      return res.locals.apicache.write.apply(this, arguments as any)
+    }
+
+    res.end = function (content: any, encoding: BufferEncoding) {
+      if (self.shouldCacheResponse(res)) {
+        self.accumulateContent(res, content)
+
+        if (res.locals.apicache.cacheable && res.locals.apicache.content) {
+          self.addIndexEntries(key)
+
+          const headers = res.locals.apicache.headers || res.getHeaders()
+          const cacheObject = self.createCacheObject(
+            res.statusCode,
+            headers,
+            res.locals.apicache.content,
+            encoding
+          )
+          self.cacheResponse(key, cacheObject, duration)
+        }
+      }
+
+      res.locals.apicache.end.apply(this, arguments as any)
+    } as any
+
+    next()
+  }
+
+  private sendCachedResponse (request: express.Request, response: express.Response, cacheObject: CacheObject, duration: number) {
+    const headers = response.getHeaders()
+
+    if (isTestInstance()) {
+      Object.assign(headers, {
+        'x-api-cache-cached': 'true'
+      })
+    }
+
+    Object.assign(headers, this.filterBlacklistedHeaders(cacheObject.headers || {}), {
+      // Set properly decremented max-age header
+      // This ensures that max-age is in sync with the cache expiration
+      'cache-control':
+        'max-age=' +
+        Math.max(
+          0,
+          (duration / 1000 - (new Date().getTime() / 1000 - cacheObject.timestamp))
+        ).toFixed(0)
+    })
+
+    // unstringify buffers
+    let data = cacheObject.data
+    if (data && data.type === 'Buffer') {
+      data = typeof data.data === 'number'
+        ? Buffer.alloc(data.data)
+        : Buffer.from(data.data)
+    }
+
+    // Test Etag against If-None-Match for 304
+    const cachedEtag = cacheObject.headers.etag
+    const requestEtag = request.headers['if-none-match']
+
+    if (requestEtag && cachedEtag === requestEtag) {
+      response.writeHead(304, headers)
+      return response.end()
+    }
+
+    response.writeHead(cacheObject.status || 200, headers)
+
+    return response.end(data, cacheObject.encoding)
+  }
+
+  private clear (target: string) {
+    const redis = Redis.Instance.getClient()
+
+    if (target) {
+      clearTimeout(this.timers[target])
+      delete this.timers[target]
+
+      try {
+        redis.del(target)
+      } catch (err) {
+        logger.error('Cannot delete %s in redis cache.', target, { err })
+      }
+
+      this.index.all = this.index.all.filter(key => key !== target)
+    } else {
+      for (const key of this.index.all) {
+        clearTimeout(this.timers[key])
+        delete this.timers[key]
+
+        try {
+          redis.del(key)
+        } catch (err) {
+          logger.error('Cannot delete %s in redis cache.', key, { err })
+        }
+      }
+
+      this.index.all = []
+    }
+
+    return this.index
+  }
+}
diff --git a/server/middlewares/cache/shared/index.ts b/server/middlewares/cache/shared/index.ts
new file mode 100644 (file)
index 0000000..c707eaf
--- /dev/null
@@ -0,0 +1 @@
+export * from './api-cache'
index e3eb1c8f54b8bc94d5943e692d9f9cfb503f980d..af5a9c29a00e58ff887d879837d4959374ebf4df 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 
 function apiFailMiddleware (req: express.Request, res: express.Response, next: express.NextFunction) {
   res.fail = options => {
index 413653dac65517dbfeaddce9ff2dbb45b089d0b6..a0035f623ad9aae55a100bd0afa64fc4567206f9 100644 (file)
@@ -1,4 +1,5 @@
 export * from './validators'
+export * from './cache'
 export * from './activitypub'
 export * from './async'
 export * from './auth'
index 9aa56bc93399478bd3894d899eea0f367af11cbb..cf70d901e305dadb0cc5575031df78dcb10e1eed 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
+import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
 import { getHostWithPort } from '../helpers/express-utils'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
 
 function setBodyHostsPort (req: express.Request, res: express.Response, next: express.NextFunction) {
   if (!req.body.hosts) return next()
index d1888c2d3808e27db3b61520d6d2f2531527044c..c8c694f055ebac9e9a15e7694333022855a46f7e 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { UserRight } from '../../shared'
+import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
 import { logger } from '../helpers/logger'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
 
 function ensureUserHasRight (userRight: UserRight) {
   return function (req: express.Request, res: express.Response, next: express.NextFunction) {
index c048bc6af344c899b750d762aee9813f76f49385..f4d9c3af201cb91dd959d0bebd80d748de31dd30 100644 (file)
@@ -16,7 +16,7 @@ import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID, toIntOrNull } from
 import { logger } from '@server/helpers/logger'
 import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
 import { AbuseCreate, UserRight } from '@shared/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { areValidationErrors, doesAbuseExist, doesAccountIdExist, doesCommentIdExist, doesVideoExist } from './shared'
 
 const abuseReportValidator = [
index cc6acd4b1aa401a60b9d518b5aef798c0a4a9829..d24e4427b79cf5191ca4dd364fc72c004ed7e06a 100644 (file)
@@ -1,8 +1,8 @@
 import * as express from 'express'
+import { getServerActor } from '@server/models/application/application'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { isRootActivityValid } from '../../../helpers/custom-validators/activitypub/activity'
 import { logger } from '../../../helpers/logger'
-import { getServerActor } from '@server/models/application/application'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 async function activityPubValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
   logger.debug('Checking activity pub parameters')
index 826b16fc8f30877b1b178c1486cbd07293a29249..f15b293e95f1b4a0ee73291103acca40b0ee005b 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { body, param } from 'express-validator'
 import { getServerActor } from '@server/models/application/application'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { isHostValid } from '../../helpers/custom-validators/servers'
 import { logger } from '../../helpers/logger'
 import { WEBSERVER } from '../../initializers/constants'
index 9bb95f5b705263c34a97bab79b6d0647b5f3ea08..6fec5814930e6a3ab5ee3bde1b9b025de1ba4e44 100644 (file)
@@ -1,8 +1,7 @@
 import * as express from 'express'
 import { body } from 'express-validator'
 import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { UserRight } from '@shared/models'
+import { HttpStatusCode, UserRight } from '@shared/models'
 import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model'
 import { logger } from '../../helpers/logger'
 import { areValidationErrors, doesAccountNameWithHostExist } from './shared'
index 51b8fdd19bbc771d207f5452aa888be7257a0a77..d29bebf6474737d2410b4a650a3c536beab40d98 100644 (file)
@@ -1,7 +1,6 @@
 import * as express from 'express'
 import { param, query } from 'express-validator'
-
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { isValidRSSFeed } from '../../helpers/custom-validators/feeds'
 import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helpers/custom-validators/misc'
 import { logger } from '../../helpers/logger'
index 205baca48ddb526ef08d55204429d24e0f97df2e..16abdd096c948a44777de807988134c41543726a 100644 (file)
@@ -1,18 +1,20 @@
 import * as express from 'express'
 import { body, param, query } from 'express-validator'
-import { isFollowStateValid } from '@server/helpers/custom-validators/follows'
+import { isEachUniqueHandleValid, isFollowStateValid, isRemoteHandleValid } from '@server/helpers/custom-validators/follows'
 import { loadActorUrlOrGetFromWebfinger } from '@server/lib/activitypub/actors'
+import { getRemoteNameAndHost } from '@server/lib/activitypub/follow'
 import { getServerActor } from '@server/models/application/application'
 import { MActorFollowActorsDefault } from '@server/types/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { isTestInstance } from '../../helpers/core-utils'
 import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
 import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
 import { logger } from '../../helpers/logger'
-import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
+import { WEBSERVER } from '../../initializers/constants'
 import { ActorModel } from '../../models/actor/actor'
 import { ActorFollowModel } from '../../models/actor/actor-follow'
 import { areValidationErrors } from './shared'
+import { ServerFollowCreate } from '@shared/models'
 
 const listFollowsValidator = [
   query('state')
@@ -30,29 +32,46 @@ const listFollowsValidator = [
 ]
 
 const followValidator = [
-  body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
+  body('hosts')
+    .toArray()
+    .custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
+
+  body('handles')
+    .toArray()
+    .custom(isEachUniqueHandleValid).withMessage('Should have an array of handles'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    // Force https if the administrator wants to make friends
+    // Force https if the administrator wants to follow remote actors
     if (isTestInstance() === false && WEBSERVER.SCHEME === 'http') {
       return res
         .status(HttpStatusCode.INTERNAL_SERVER_ERROR_500)
         .json({
           error: 'Cannot follow on a non HTTPS web server.'
         })
-        .end()
     }
 
     logger.debug('Checking follow parameters', { parameters: req.body })
 
     if (areValidationErrors(req, res)) return
 
+    const body: ServerFollowCreate = req.body
+    if (body.hosts.length === 0 && body.handles.length === 0) {
+
+      return res
+        .status(HttpStatusCode.BAD_REQUEST_400)
+        .json({
+          error: 'You must provide at least one handle or one host.'
+        })
+    }
+
     return next()
   }
 ]
 
 const removeFollowingValidator = [
-  param('host').custom(isHostValid).withMessage('Should have a valid host'),
+  param('hostOrHandle')
+    .custom(value => isHostValid(value) || isRemoteHandleValid(value))
+    .withMessage('Should have a valid host/handle'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking unfollowing parameters', { parameters: req.params })
@@ -60,12 +79,14 @@ const removeFollowingValidator = [
     if (areValidationErrors(req, res)) return
 
     const serverActor = await getServerActor()
-    const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host)
+
+    const { name, host } = getRemoteNameAndHost(req.params.hostOrHandle)
+    const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, name, host)
 
     if (!follow) {
       return res.fail({
         status: HttpStatusCode.NOT_FOUND_404,
-        message: `Following ${req.params.host} not found.`
+        message: `Follow ${req.params.hostOrHandle} not found.`
       })
     }
 
index 0a82e6932d77dd1b95d4c2fea4a55113705f6fe8..e5fc0c277dc498c3f60944b382532de18711d3c9 100644 (file)
@@ -4,7 +4,7 @@ import { join } from 'path'
 import { loadVideo } from '@server/lib/model-loaders'
 import { VideoPlaylistModel } from '@server/models/video/video-playlist'
 import { VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { isTestInstance } from '../../helpers/core-utils'
 import { isIdOrUUIDValid, toCompleteUUID } from '../../helpers/custom-validators/misc'
 import { logger } from '../../helpers/logger'
index 8c76d2e3633b01ca8da671be8dfece3cebec566a..3fb2176b9d6832f212d8bcf63e2eade58ac593eb 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { body, param, query, ValidationChain } from 'express-validator'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { PluginType } from '../../../shared/models/plugins/plugin.type'
 import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/server/api/install-plugin.model'
 import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
index 116c8c611188ac748858bf23ad5da6d61591a2ac..f1b2ff5cd92831409332ee671927dd27da6fe2ad 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { body, param, query } from 'express-validator'
 import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import {
   exists,
   isBooleanValid,
index 7bbf81048ede00403cc8a63c6612ce6b54f5f269..27d0e541d61dd7bbc9a4848915e3c5dcbce614ae 100644 (file)
@@ -1,13 +1,18 @@
 import * as express from 'express'
 import { query } from 'express-validator'
 import { isSearchTargetValid } from '@server/helpers/custom-validators/search'
-import { isDateValid } from '../../helpers/custom-validators/misc'
+import { isHostValid } from '@server/helpers/custom-validators/servers'
+import { areUUIDsValid, isDateValid, isNotEmptyStringArray, toCompleteUUIDs } from '../../helpers/custom-validators/misc'
 import { logger } from '../../helpers/logger'
 import { areValidationErrors } from './shared'
 
 const videosSearchValidator = [
   query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
 
+  query('host')
+    .optional()
+    .custom(isHostValid).withMessage('Should have a valid host'),
+
   query('startDate')
     .optional()
     .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
@@ -22,8 +27,18 @@ const videosSearchValidator = [
     .optional()
     .custom(isDateValid).withMessage('Should have a published end date that conforms to ISO 8601'),
 
-  query('durationMin').optional().isInt().withMessage('Should have a valid min duration'),
-  query('durationMax').optional().isInt().withMessage('Should have a valid max duration'),
+  query('durationMin')
+    .optional()
+    .isInt().withMessage('Should have a valid min duration'),
+  query('durationMax')
+    .optional()
+    .isInt().withMessage('Should have a valid max duration'),
+
+  query('uuids')
+    .optional()
+    .toArray()
+    .customSanitizer(toCompleteUUIDs)
+    .custom(areUUIDsValid).withMessage('Should have valid uuids'),
 
   query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'),
 
@@ -37,8 +52,22 @@ const videosSearchValidator = [
 ]
 
 const videoChannelsListSearchValidator = [
-  query('search').not().isEmpty().withMessage('Should have a valid search'),
-  query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'),
+  query('search')
+    .optional()
+    .not().isEmpty().withMessage('Should have a valid search'),
+
+  query('host')
+    .optional()
+    .custom(isHostValid).withMessage('Should have a valid host'),
+
+  query('searchTarget')
+    .optional()
+    .custom(isSearchTargetValid).withMessage('Should have a valid search target'),
+
+  query('handles')
+    .optional()
+    .toArray()
+    .custom(isNotEmptyStringArray).withMessage('Should have valid handles'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking video channels search query', { parameters: req.query })
@@ -50,8 +79,23 @@ const videoChannelsListSearchValidator = [
 ]
 
 const videoPlaylistsListSearchValidator = [
-  query('search').not().isEmpty().withMessage('Should have a valid search'),
-  query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'),
+  query('search')
+    .optional()
+    .not().isEmpty().withMessage('Should have a valid search'),
+
+  query('host')
+    .optional()
+    .custom(isHostValid).withMessage('Should have a valid host'),
+
+  query('searchTarget')
+    .optional()
+    .custom(isSearchTargetValid).withMessage('Should have a valid search target'),
+
+  query('uuids')
+    .optional()
+    .toArray()
+    .customSanitizer(toCompleteUUIDs)
+    .custom(areUUIDsValid).withMessage('Should have valid uuids'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking video playlists search query', { parameters: req.query })
index fc7239b25203fb411a70a25597f1e0c2f4a32e50..29fdc13d28cc5a49a541308426e6f72e2b92f166 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { body } from 'express-validator'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers'
 import { isUserDisplayNameValid } from '../../helpers/custom-validators/users'
 import { logger } from '../../helpers/logger'
index 4a20a55fae90ab3044334ce7815d90165f5a38e1..2b8d86ba519702141711fdfdeb804785159b26a9 100644 (file)
@@ -1,6 +1,6 @@
 import { Response } from 'express'
 import { AbuseModel } from '@server/models/abuse/abuse'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 
 async function doesAbuseExist (abuseId: number | string, res: Response) {
   const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10))
index 04da15441fa9ad689c591400eaa951475f4fab7e..fe4f83aa0071669e7541690302b7ec23a5f55517 100644 (file)
@@ -2,7 +2,7 @@ import { Response } from 'express'
 import { AccountModel } from '@server/models/account/account'
 import { UserModel } from '@server/models/user/user'
 import { MAccountDefault } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 
 function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
   const promise = AccountModel.load(parseInt(id + '', 10))
index 01491c10f60a4b8dbb496ffd0b3761629de87bdd..f85b39b232c580034dbd69fbd34e6b21691d4e4d 100644 (file)
@@ -1,6 +1,6 @@
 import { Response } from 'express'
 import { VideoBlacklistModel } from '@server/models/video/video-blacklist'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 
 async function doesVideoBlacklistExist (videoId: number, res: Response) {
   const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
index 80f6c5a5246a1af7094c4914885b4999a2df8fd1..831b366eaed9539efdf0582a116ffaf3c125a56a 100644 (file)
@@ -1,7 +1,7 @@
 import { Response } from 'express'
 import { VideoCaptionModel } from '@server/models/video/video-caption'
 import { MVideoId } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 
 async function doesVideoCaptionExist (video: MVideoId, language: string, res: Response) {
   const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
index fe2e663b754b4f30d52270645eeb0bd7c2c4d99d..3fc3d012a6e490e692d1dcea42205d2d7f84ba5c 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { VideoChannelModel } from '@server/models/video/video-channel'
 import { MChannelBannerAccountDefault } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 
 async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
   const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
index 83ea15c988a0ce1a8ca8092eac382f474d536918..60132fb6e0bc45869452a6376f5efc1ec11d60b1 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { VideoCommentModel } from '@server/models/video/video-comment'
 import { MVideoId } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 
 async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
   const id = parseInt(idArg + '', 10)
index 0f984bc172b89ca6c4d93faffc31fcc100cd18b1..50b49ffcb61b2449fa15759d8c636594fc2d8978 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { VideoImportModel } from '@server/models/video/video-import'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 
 async function doesVideoImportExist (id: number, res: express.Response) {
   const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
index fc27006ce085a41779f2a220810542ccf1e0bc44..93a23ef40ee6f0985502eff43c2dd35c6e3c657d 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 
 async function doesChangeVideoOwnershipExist (idArg: number | string, res: express.Response) {
   const id = parseInt(idArg + '', 10)
index d762859a84bd5f003fa7e6d6d509a350bd18f546..3f67681797be98f47dac099e8e5081453761a2e9 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { VideoPlaylistModel } from '@server/models/video/video-playlist'
 import { MVideoPlaylist } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
 
 export type VideoPlaylistFetchType = 'summary' | 'all'
 async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: VideoPlaylistFetchType = 'summary') {
index 2c66c1a3ad27b17ed0d4540685b7d5b6b6eb5470..71b81654f582ab24d95828d134539173c2b1be46 100644 (file)
@@ -12,8 +12,7 @@ import {
   MVideoImmutable,
   MVideoThumbnail
 } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
-import { UserRight } from '@shared/models'
+import { HttpStatusCode, UserRight } from '@shared/models'
 
 async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
   const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
index d4716257f9a093791eaba1e5ccba60037e5fc885..2953b9505bbc3930b902ca461ca555bcbfe4a362 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { param } from 'express-validator'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { isSafePath } from '../../helpers/custom-validators/misc'
 import { isPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
 import { logger } from '../../helpers/logger'
index ab7962923f7eceed492598d4317a7a7fb93b9605..df57777714b4c75e1703091606fef1dd366ac2f0 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { body, param, query } from 'express-validator'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
 import { toArray } from '../../helpers/custom-validators/misc'
 import { logger } from '../../helpers/logger'
index 698d7d814b7a7daf72c592be9f26c133c5356561..748b89f8fd7f755a863941da496ae0d4a02ed33e 100644 (file)
@@ -3,7 +3,7 @@ import { body, param, query } from 'express-validator'
 import { omit } from 'lodash'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { MUserDefault } from '@server/types/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { UserRole } from '../../../shared/models/users'
 import { UserRegister } from '../../../shared/models/users/user-register.model'
 import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
index 21141d84d1204f009fbd4c7fa6e7b6e91b3521c5..3a4937b7b610a37cb0f2b6566dc2c44659f41d5c 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { body, query } from 'express-validator'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { isBooleanValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
 import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../../helpers/custom-validators/video-blacklist'
 import { logger } from '../../../helpers/logger'
index e7df185e4f4c5851b994240a7dd2184d13b9435f..ea10fe42557ee7cca0da489f07f5a1310a478cc3 100644 (file)
@@ -3,7 +3,7 @@ import { body, param, query } from 'express-validator'
 import { VIDEO_CHANNELS } from '@server/initializers/constants'
 import { MChannelAccountDefault, MUser } from '@server/types/models'
 import { UserRight } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
 import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
 import {
index 885506ebe691fe21cc9aab718ef3318bb115c8ac..61c2ed92f857946e7c1f147596c37867e98357ae 100644 (file)
@@ -2,7 +2,7 @@ import * as express from 'express'
 import { body, param, query } from 'express-validator'
 import { MUserAccountUrl } from '@server/types/models'
 import { UserRight } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
 import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
 import { logger } from '../../../helpers/logger'
index 85dc647cef8f87f54c1fab77f14f44070ec08fb3..52b839e5692c4a265e688c8494438c53c38db28f 100644 (file)
@@ -2,7 +2,7 @@ import * as express from 'express'
 import { body } from 'express-validator'
 import { isPreImportVideoAccepted } from '@server/lib/moderation'
 import { Hooks } from '@server/lib/plugins/hooks'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '@shared/models'
 import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model'
 import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
 import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
index 7cfb935e306d1c6089c247519a3b64963804e1b4..97e8b4510a8993eec8026efa57d8c6d6231fb3a0 100644 (file)
@@ -5,8 +5,7 @@ import { isLocalLiveVideoAccepted } from '@server/lib/moderation'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { VideoModel } from '@server/models/video/video'
 import { VideoLiveModel } from '@server/models/video/video-live'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { ServerErrorCode, UserRight, VideoState } from '@shared/models'
+import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@shared/models'
 import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
 import { isVideoNameValid } from '../../../helpers/custom-validators/videos'
 import { cleanUpReqFiles } from '../../../helpers/express-utils'
index 54ac46c99cd2c45ef54a059fa386aab485e8e819..a7f0b72c38932e2652e6914a2979e62f1b7d45bd 100644 (file)
@@ -6,8 +6,14 @@ import { logger } from '@server/helpers/logger'
 import { isAbleToUploadVideo } from '@server/lib/user'
 import { AccountModel } from '@server/models/account/account'
 import { MVideoWithAllFiles } from '@server/types/models'
-import { HttpStatusCode } from '@shared/core-utils'
-import { ServerErrorCode, UserRight, VideoChangeOwnershipAccept, VideoChangeOwnershipStatus, VideoState } from '@shared/models'
+import {
+  HttpStatusCode,
+  ServerErrorCode,
+  UserRight,
+  VideoChangeOwnershipAccept,
+  VideoChangeOwnershipStatus,
+  VideoState
+} from '@shared/models'
 import {
   areValidationErrors,
   checkUserCanManageVideo,
index 5ee7ee0ce54f561636ac27f7e9f481c3a288afae..ab84b4814aa010b450df9a6d6d6fcda446a8203f 100644 (file)
@@ -3,7 +3,7 @@ import { body, param, query, ValidationChain } from 'express-validator'
 import { ExpressPromiseHandler } from '@server/types/express'
 import { MUserAccountId } from '@server/types/models'
 import { UserRight, VideoPlaylistCreate, VideoPlaylistUpdate } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
 import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
 import {
index 5d5dfb2227c5b18807d16e99655e9a2e17857f09..5fe78b39e86caf2bf6d1c6d8fdca0bc825bc3c24 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { body, param, query } from 'express-validator'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { VideoRateType } from '../../../../shared/models/videos'
 import { isAccountNameValid } from '../../../helpers/custom-validators/accounts'
 import { isIdValid } from '../../../helpers/custom-validators/misc'
index 7e54b6fc0f6237d6a018083e2e9d2e9d52d3a4d4..3b8d6176801dbb4497fb3f38c19b5699297f9793 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { param } from 'express-validator'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { isIdValid } from '../../../helpers/custom-validators/misc'
 import { logger } from '../../../helpers/logger'
 import { VideoShareModel } from '../../../models/video/video-share'
index 43306f7cda9a39a0dd63c1b52de7dab0d7dc4206..431515eb15a90303969f93049baa9e902ab6b36d 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { body } from 'express-validator'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { toIntOrNull } from '../../../helpers/custom-validators/misc'
 import { logger } from '../../../helpers/logger'
 import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
index 49e10e2b5497d5751d43664ace446c5208de590f..374a59c50d850c13a83147b1d5b4f81f5045760a 100644 (file)
@@ -6,7 +6,7 @@ import { getServerActor } from '@server/models/application/application'
 import { ExpressPromiseHandler } from '@server/types/express'
 import { MUserAccountId, MVideoFullLight } from '@server/types/models'
 import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import {
   exists,
   isBooleanValid,
index bcdd136c6ae3123a2f15577ed4492774440ebb70..1313608203b811f892aed39cb71f82f10d3136cb 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { query } from 'express-validator'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger'
 import { getHostWithPort } from '../../helpers/express-utils'
 import { logger } from '../../helpers/logger'
index 665ecd595cf604a2b8c13e1b036028a4110f5fd2..37194a119bb85b6ff1ecbc8d3919c25e8f1c7f4e 100644 (file)
@@ -52,6 +52,7 @@ export enum ScopeNames {
 export type SummaryOptions = {
   actorRequired?: boolean // Default: true
   whereActor?: WhereOptions
+  whereServer?: WhereOptions
   withAccountBlockerIds?: number[]
 }
 
@@ -65,12 +66,11 @@ export type SummaryOptions = {
 }))
 @Scopes(() => ({
   [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
-    const whereActor = options.whereActor || undefined
-
     const serverInclude: IncludeOptions = {
       attributes: [ 'host' ],
       model: ServerModel.unscoped(),
-      required: false
+      required: !!options.whereServer,
+      where: options.whereServer
     }
 
     const queryInclude: Includeable[] = [
@@ -78,7 +78,7 @@ export type SummaryOptions = {
         attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
         model: ActorModel.unscoped(),
         required: options.actorRequired ?? true,
-        where: whereActor,
+        where: options.whereActor,
         include: [
           serverInclude,
 
index 3a09e51d6feffae7bd59ddba02cb7a9af01b8afb..283856d3ffa73b3482997400cdde6f9cc74dc4d4 100644 (file)
@@ -20,7 +20,6 @@ import {
 } from 'sequelize-typescript'
 import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
 import { getServerActor } from '@server/models/application/application'
-import { VideoModel } from '@server/models/video/video'
 import {
   MActorFollowActorsDefault,
   MActorFollowActorsDefaultSubscription,
@@ -36,6 +35,7 @@ import { logger } from '../../helpers/logger'
 import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
 import { AccountModel } from '../account/account'
 import { ServerModel } from '../server/server'
+import { doesExist } from '../shared/query'
 import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils'
 import { VideoChannelModel } from '../video/video-channel'
 import { ActorModel, unusedActorAttributesForAPI } from './actor'
@@ -166,14 +166,8 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
 
   static isFollowedBy (actorId: number, followerActorId: number) {
     const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1'
-    const options = {
-      type: QueryTypes.SELECT as QueryTypes.SELECT,
-      bind: { actorId, followerActorId },
-      raw: true
-    }
 
-    return VideoModel.sequelize.query(query, options)
-                     .then(results => results.length === 1)
+    return doesExist(query, { actorId, followerActorId })
   }
 
   static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> {
@@ -324,13 +318,13 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
 
     const followWhere = state ? { state } : {}
     const followingWhere: WhereOptions = {}
-    const followingServerWhere: WhereOptions = {}
 
     if (search) {
-      Object.assign(followingServerWhere, {
-        host: {
-          [Op.iLike]: '%' + search + '%'
-        }
+      Object.assign(followWhere, {
+        [Op.or]: [
+          searchAttribute(options.search, '$ActorFollowing.preferredUsername$'),
+          searchAttribute(options.search, '$ActorFollowing.Server.host$')
+        ]
       })
     }
 
@@ -361,8 +355,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
           include: [
             {
               model: ServerModel,
-              required: true,
-              where: followingServerWhere
+              required: true
             }
           ]
         }
@@ -391,13 +384,13 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
 
     const followWhere = state ? { state } : {}
     const followerWhere: WhereOptions = {}
-    const followerServerWhere: WhereOptions = {}
 
     if (search) {
-      Object.assign(followerServerWhere, {
-        host: {
-          [Op.iLike]: '%' + search + '%'
-        }
+      Object.assign(followWhere, {
+        [Op.or]: [
+          searchAttribute(search, '$ActorFollower.preferredUsername$'),
+          searchAttribute(search, '$ActorFollower.Server.host$')
+        ]
       })
     }
 
@@ -420,8 +413,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
           include: [
             {
               model: ServerModel,
-              required: true,
-              where: followerServerWhere
+              required: true
             }
           ]
         },
index ccda023e018388bcfabb7a5f4153946c6cd0ffb6..d645be24813633dbee752cc3cc408e1fa01255b0 100644 (file)
@@ -160,8 +160,8 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu
       const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
       logger.info('Removing duplicated video file %s.', logIdentifier)
 
-      videoFile.Video.removeFile(videoFile, true)
-               .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
+      videoFile.Video.removeFileAndTorrent(videoFile, true)
+        .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
     }
 
     if (instance.videoStreamingPlaylistId) {
diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts
new file mode 100644 (file)
index 0000000..5b97510
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './query'
+export * from './update'
diff --git a/server/models/shared/query.ts b/server/models/shared/query.ts
new file mode 100644 (file)
index 0000000..036cc13
--- /dev/null
@@ -0,0 +1,17 @@
+import { BindOrReplacements, QueryTypes } from 'sequelize'
+import { sequelizeTypescript } from '@server/initializers/database'
+
+function doesExist (query: string, bind?: BindOrReplacements) {
+  const options = {
+    type: QueryTypes.SELECT as QueryTypes.SELECT,
+    bind,
+    raw: true
+  }
+
+  return sequelizeTypescript.query(query, options)
+            .then(results => results.length === 1)
+}
+
+export {
+  doesExist
+}
diff --git a/server/models/shared/update.ts b/server/models/shared/update.ts
new file mode 100644 (file)
index 0000000..d338211
--- /dev/null
@@ -0,0 +1,18 @@
+import { QueryTypes, Transaction } from 'sequelize'
+import { sequelizeTypescript } from '@server/initializers/database'
+
+// Sequelize always skip the update if we only update updatedAt field
+function setAsUpdated (table: string, id: number, transaction?: Transaction) {
+  return sequelizeTypescript.query(
+    `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`,
+    {
+      replacements: { table, id, updatedAt: new Date() },
+      type: QueryTypes.UPDATE,
+      transaction
+    }
+  )
+}
+
+export {
+  setAsUpdated
+}
index a7f84e9cabee0447a7afe57d8f376b8e4548cbe2..04c5513a9b92b500496994ed5b822c6887575be9 100644 (file)
@@ -1,5 +1,6 @@
 import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import { uuidToShort } from '@server/helpers/uuid'
 import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
 import { AttributesOnly } from '@shared/core-utils'
 import { UserNotification, UserNotificationType } from '../../../shared'
@@ -615,6 +616,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
     return {
       id: video.id,
       uuid: video.uuid,
+      shortUUID: uuidToShort(video.uuid),
       name: video.name
     }
   }
@@ -628,6 +630,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
           ? {
             id: abuse.VideoCommentAbuse.VideoComment.Video.id,
             name: abuse.VideoCommentAbuse.VideoComment.Video.name,
+            shortUUID: uuidToShort(abuse.VideoCommentAbuse.VideoComment.Video.uuid),
             uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
           }
           : undefined
index ab4cf53a813cfc28a71ce14f2be4cc3f37016e84..8a54de3b031e13c475b160eda7f86376c3921fd7 100644 (file)
@@ -182,8 +182,8 @@ function streamingPlaylistsModelToFormattedJSON (
       return {
         id: playlist.id,
         type: playlist.type,
-        playlistUrl: playlist.playlistUrl,
-        segmentsSha256Url: playlist.segmentsSha256Url,
+        playlistUrl: playlist.getMasterPlaylistUrl(video),
+        segmentsSha256Url: playlist.getSha256SegmentsUrl(video),
         redundancies,
         files
       }
@@ -331,7 +331,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
       type: 'Link',
       name: 'sha256',
       mediaType: 'application/json' as 'application/json',
-      href: playlist.segmentsSha256Url
+      href: playlist.getSha256SegmentsUrl(video)
     })
 
     addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || [])
@@ -339,7 +339,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
     url.push({
       type: 'Link',
       mediaType: 'application/x-mpegURL' as 'application/x-mpegURL',
-      href: playlist.playlistUrl,
+      href: playlist.getMasterPlaylistUrl(video),
       tag
     })
   }
index abdd22188d2c5500650aaa7b535f8a6bc61f1ea4..742d19099e8cd653a070f622668b5e4c4d29bcd8 100644 (file)
@@ -92,12 +92,13 @@ export class VideoTables {
   }
 
   getStreamingPlaylistAttributes () {
-    let playlistKeys = [ 'id', 'playlistUrl', 'type' ]
+    let playlistKeys = [ 'id', 'playlistUrl', 'playlistFilename', 'type' ]
 
     if (this.mode === 'get') {
       playlistKeys = playlistKeys.concat([
         'p2pMediaLoaderInfohashes',
         'p2pMediaLoaderPeerVersion',
+        'segmentsSha256Filename',
         'segmentsSha256Url',
         'videoId',
         'createdAt',
index 30b251f0f33ce3123922ca1f6fb143a8dd0ff44b..7625c003d0fbc6cc4d9a90f3951e7d0798987c91 100644 (file)
@@ -1,6 +1,7 @@
 import { Sequelize } from 'sequelize'
 import validator from 'validator'
 import { exists } from '@server/helpers/custom-validators/misc'
+import { WEBSERVER } from '@server/initializers/constants'
 import { buildDirectionAndField, createSafeIn } from '@server/models/utils'
 import { MUserAccountId, MUserId } from '@server/types/models'
 import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models'
@@ -25,6 +26,7 @@ export type BuildVideosListQueryOptions = {
 
   nsfw?: boolean
   filter?: VideoFilter
+  host?: string
   isLive?: boolean
 
   categoryOneOf?: number[]
@@ -33,6 +35,8 @@ export type BuildVideosListQueryOptions = {
   tagsOneOf?: string[]
   tagsAllOf?: string[]
 
+  uuids?: string[]
+
   withFiles?: boolean
 
   accountId?: number
@@ -131,6 +135,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
       this.whereOnlyLocal()
     }
 
+    if (options.host) {
+      this.whereHost(options.host)
+    }
+
     if (options.accountId) {
       this.whereAccountId(options.accountId)
     }
@@ -155,6 +163,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
       this.whereTagsAllOf(options.tagsAllOf)
     }
 
+    if (options.uuids) {
+      this.whereUUIDs(options.uuids)
+    }
+
     if (options.nsfw === true) {
       this.whereNSFW()
     } else if (options.nsfw === false) {
@@ -291,6 +303,19 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
     this.and.push('"video"."remote" IS FALSE')
   }
 
+  private whereHost (host: string) {
+    // Local instance
+    if (host === WEBSERVER.HOST) {
+      this.and.push('"accountActor"."serverId" IS NULL')
+      return
+    }
+
+    this.joins.push('INNER JOIN "server" ON "server"."id" = "accountActor"."serverId"')
+
+    this.and.push('"server"."host" = :host')
+    this.replacements.host = host
+  }
+
   private whereAccountId (accountId: number) {
     this.and.push('"account"."id" = :accountId')
     this.replacements.accountId = accountId
@@ -304,16 +329,16 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
   private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) {
     let query =
     '(' +
-    '  EXISTS (' +
+    '  EXISTS (' + // Videos shared by actors we follow
     '    SELECT 1 FROM "videoShare" ' +
     '    INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' +
     '    AND "actorFollowShare"."actorId" = :followerActorId AND "actorFollowShare"."state" = \'accepted\' ' +
     '    WHERE "videoShare"."videoId" = "video"."id"' +
     '  )' +
     '  OR' +
-    '  EXISTS (' +
+    '  EXISTS (' + // Videos published by accounts we follow
     '    SELECT 1 from "actorFollow" ' +
-    '    WHERE "actorFollow"."targetActorId" = "videoChannel"."actorId" AND "actorFollow"."actorId" = :followerActorId ' +
+    '    WHERE "actorFollow"."targetActorId" = "account"."actorId" AND "actorFollow"."actorId" = :followerActorId ' +
     '    AND "actorFollow"."state" = \'accepted\'' +
     '  )'
 
@@ -367,6 +392,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
     )
   }
 
+  private whereUUIDs (uuids: string[]) {
+    this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')')
+  }
+
   private whereCategoryOneOf (categoryOneOf: number[]) {
     this.and.push('"video"."category" IN (:categoryOneOf)')
     this.replacements.categoryOneOf = categoryOneOf
index 183e7448c4d06155ff8304d3085de57dc6b67d80..9f04a57c64025f11baa6e9d02fee6f4ef1208141 100644 (file)
@@ -1,4 +1,4 @@
-import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize'
+import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
 import {
   AllowNull,
   BeforeDestroy,
@@ -17,9 +17,8 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { setAsUpdated } from '@server/helpers/database-utils'
 import { MAccountActor } from '@server/types/models'
-import { AttributesOnly } from '@shared/core-utils'
+import { AttributesOnly, pick } from '@shared/core-utils'
 import { ActivityPubActor } from '../../../shared/models/activitypub'
 import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
 import {
@@ -41,6 +40,7 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
 import { ActorFollowModel } from '../actor/actor-follow'
 import { ActorImageModel } from '../actor/actor-image'
 import { ServerModel } from '../server/server'
+import { setAsUpdated } from '../shared'
 import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 import { VideoPlaylistModel } from './video-playlist'
@@ -58,6 +58,8 @@ export enum ScopeNames {
 type AvailableForListOptions = {
   actorId: number
   search?: string
+  host?: string
+  handles?: string[]
 }
 
 type AvailableWithStatsOptions = {
@@ -83,7 +85,62 @@ export type SummaryOptions = {
     // Only list local channels OR channels that are on an instance followed by actorId
     const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
 
+    const whereActorAnd: WhereOptions[] = [
+      {
+        [Op.or]: [
+          {
+            serverId: null
+          },
+          {
+            serverId: {
+              [Op.in]: Sequelize.literal(inQueryInstanceFollow)
+            }
+          }
+        ]
+      }
+    ]
+
+    let serverRequired = false
+    let whereServer: WhereOptions
+
+    if (options.host && options.host !== WEBSERVER.HOST) {
+      serverRequired = true
+      whereServer = { host: options.host }
+    }
+
+    if (options.host === WEBSERVER.HOST) {
+      whereActorAnd.push({
+        serverId: null
+      })
+    }
+
+    let rootWhere: WhereOptions
+    if (options.handles) {
+      const or: WhereOptions[] = []
+
+      for (const handle of options.handles || []) {
+        const [ preferredUsername, host ] = handle.split('@')
+
+        if (!host) {
+          or.push({
+            '$Actor.preferredUsername$': preferredUsername,
+            '$Actor.serverId$': null
+          })
+        } else {
+          or.push({
+            '$Actor.preferredUsername$': preferredUsername,
+            '$Actor.Server.host$': host
+          })
+        }
+      }
+
+      rootWhere = {
+        [Op.or]: or
+      }
+    }
+
     return {
+      where: rootWhere,
       include: [
         {
           attributes: {
@@ -91,18 +148,19 @@ export type SummaryOptions = {
           },
           model: ActorModel,
           where: {
-            [Op.or]: [
-              {
-                serverId: null
-              },
-              {
-                serverId: {
-                  [Op.in]: Sequelize.literal(inQueryInstanceFollow)
-                }
-              }
-            ]
+            [Op.and]: whereActorAnd
           },
           include: [
+            {
+              model: ServerModel,
+              required: serverRequired,
+              where: whereServer
+            },
+            {
+              model: ActorImageModel,
+              as: 'Avatar',
+              required: false
+            },
             {
               model: ActorImageModel,
               as: 'Banner',
@@ -380,30 +438,6 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
     }
   }
 
-  static listForApi (parameters: {
-    actorId: number
-    start: number
-    count: number
-    sort: string
-  }) {
-    const { actorId } = parameters
-
-    const query = {
-      offset: parameters.start,
-      limit: parameters.count,
-      order: getSort(parameters.sort)
-    }
-
-    return VideoChannelModel
-      .scope({
-        method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
-      })
-      .findAndCountAll(query)
-      .then(({ rows, count }) => {
-        return { total: count, data: rows }
-      })
-  }
-
   static listLocalsForSitemap (sort: string): Promise<MChannelActor[]> {
     const query = {
       attributes: [ ],
@@ -425,26 +459,43 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       .findAll(query)
   }
 
-  static searchForApi (options: {
-    actorId: number
-    search: string
+  static listForApi (parameters: Pick<AvailableForListOptions, 'actorId'> & {
     start: number
     count: number
     sort: string
   }) {
-    const attributesInclude = []
-    const escapedSearch = VideoChannelModel.sequelize.escape(options.search)
-    const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%')
-    attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search))
+    const { actorId } = parameters
 
     const query = {
-      attributes: {
-        include: attributesInclude
-      },
-      offset: options.start,
-      limit: options.count,
-      order: getSort(options.sort),
-      where: {
+      offset: parameters.start,
+      limit: parameters.count,
+      order: getSort(parameters.sort)
+    }
+
+    return VideoChannelModel
+      .scope({
+        method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
+      })
+      .findAndCountAll(query)
+      .then(({ rows, count }) => {
+        return { total: count, data: rows }
+      })
+  }
+
+  static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & {
+    start: number
+    count: number
+    sort: string
+  }) {
+    let attributesInclude: any[] = [ literal('0 as similarity') ]
+    let where: WhereOptions
+
+    if (options.search) {
+      const escapedSearch = VideoChannelModel.sequelize.escape(options.search)
+      const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%')
+      attributesInclude = [ createSimilarityAttribute('VideoChannelModel.name', options.search) ]
+
+      where = {
         [Op.or]: [
           Sequelize.literal(
             'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))'
@@ -456,9 +507,19 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       }
     }
 
+    const query = {
+      attributes: {
+        include: attributesInclude
+      },
+      offset: options.start,
+      limit: options.count,
+      order: getSort(options.sort),
+      where
+    }
+
     return VideoChannelModel
       .scope({
-        method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ]
+        method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ]
       })
       .findAndCountAll(query)
       .then(({ rows, count }) => {
index 22cf638046afb6e3fb1ee0bf2e33285cc5484859..09fc5288bffe60b4793ea3ef1a292793fee47012 100644 (file)
@@ -1,7 +1,7 @@
 import { remove } from 'fs-extra'
 import * as memoizee from 'memoizee'
 import { join } from 'path'
-import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
+import { FindOptions, Op, Transaction } from 'sequelize'
 import {
   AllowNull,
   BelongsTo,
@@ -44,6 +44,7 @@ import {
 } from '../../initializers/constants'
 import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
+import { doesExist } from '../shared'
 import { parseAggregateResult, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
@@ -250,14 +251,8 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
 
   static doesInfohashExist (infoHash: string) {
     const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
-    const options = {
-      type: QueryTypes.SELECT as QueryTypes.SELECT,
-      bind: { infoHash },
-      raw: true
-    }
 
-    return VideoModel.sequelize.query(query, options)
-              .then(results => results.length === 1)
+    return doesExist(query, { infoHash })
   }
 
   static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
@@ -266,6 +261,33 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
     return !!videoFile
   }
 
+  static async doesOwnedTorrentFileExist (filename: string) {
+    const query = 'SELECT 1 FROM "videoFile" ' +
+                  'LEFT JOIN "video" "webtorrent" ON "webtorrent"."id" = "videoFile"."videoId" AND "webtorrent"."remote" IS FALSE ' +
+                  'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' +
+                  'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' +
+                  'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1'
+
+    return doesExist(query, { filename })
+  }
+
+  static async doesOwnedWebTorrentVideoFileExist (filename: string) {
+    const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' +
+                  'WHERE "filename" = $filename LIMIT 1'
+
+    return doesExist(query, { filename })
+  }
+
+  static loadByFilename (filename: string) {
+    const query = {
+      where: {
+        filename
+      }
+    }
+
+    return VideoFileModel.findOne(query)
+  }
+
   static loadWithVideoOrPlaylistByTorrentFilename (filename: string) {
     const query = {
       where: {
@@ -443,10 +465,9 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
   }
 
   getFileDownloadUrl (video: MVideoWithHost) {
-    const basePath = this.isHLS()
-      ? STATIC_DOWNLOAD_PATHS.HLS_VIDEOS
-      : STATIC_DOWNLOAD_PATHS.VIDEOS
-    const path = join(basePath, this.filename)
+    const path = this.isHLS()
+      ? join(STATIC_DOWNLOAD_PATHS.HLS_VIDEOS, `${video.uuid}-${this.resolution}-fragmented${this.extname}`)
+      : join(STATIC_DOWNLOAD_PATHS.VIDEOS, `${video.uuid}-${this.resolution}${this.extname}`)
 
     if (video.isOwned()) return WEBSERVER.URL + path
 
index af81c9906808f24e8b1b17ce9354e057d1a317e0..630684a88350d04f5ffe9d20026c5ecdf5622144 100644 (file)
@@ -17,10 +17,9 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { setAsUpdated } from '@server/helpers/database-utils'
 import { buildUUID, uuidToShort } from '@server/helpers/uuid'
 import { MAccountId, MChannelId } from '@server/types/models'
-import { AttributesOnly } from '@shared/core-utils'
+import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils'
 import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
 import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
 import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
@@ -53,6 +52,7 @@ import {
 } from '../../types/models/video/video-playlist'
 import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
 import { ActorModel } from '../actor/actor'
+import { setAsUpdated } from '../shared'
 import {
   buildServerIdsFollowedBy,
   buildTrigramSearchIndex,
@@ -82,6 +82,8 @@ type AvailableForListOptions = {
   videoChannelId?: number
   listMyPlaylists?: boolean
   search?: string
+  host?: string
+  uuids?: string[]
   withVideos?: boolean
 }
 
@@ -141,9 +143,19 @@ function getVideoLengthSelect () {
     ]
   },
   [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
+    const whereAnd: WhereOptions[] = []
+
+    const whereServer = options.host && options.host !== WEBSERVER.HOST
+      ? { host: options.host }
+      : undefined
+
     let whereActor: WhereOptions = {}
 
-    const whereAnd: WhereOptions[] = []
+    if (options.host === WEBSERVER.HOST) {
+      whereActor = {
+        [Op.and]: [ { serverId: null } ]
+      }
+    }
 
     if (options.listMyPlaylists !== true) {
       whereAnd.push({
@@ -168,9 +180,7 @@ function getVideoLengthSelect () {
         })
       }
 
-      whereActor = {
-        [Op.or]: whereActorOr
-      }
+      Object.assign(whereActor, { [Op.or]: whereActorOr })
     }
 
     if (options.accountId) {
@@ -191,18 +201,26 @@ function getVideoLengthSelect () {
       })
     }
 
+    if (options.uuids) {
+      whereAnd.push({
+        uuid: {
+          [Op.in]: options.uuids
+        }
+      })
+    }
+
     if (options.withVideos === true) {
       whereAnd.push(
         literal(`(${getVideoLengthSelect()}) != 0`)
       )
     }
 
-    const attributesInclude = []
+    let attributesInclude: any[] = [ literal('0 as similarity') ]
 
     if (options.search) {
       const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search)
       const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%')
-      attributesInclude.push(createSimilarityAttribute('VideoPlaylistModel.name', options.search))
+      attributesInclude = [ createSimilarityAttribute('VideoPlaylistModel.name', options.search) ]
 
       whereAnd.push({
         [Op.or]: [
@@ -228,7 +246,7 @@ function getVideoLengthSelect () {
       include: [
         {
           model: AccountModel.scope({
-            method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ]
+            method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer } as SummaryOptions ]
           }),
           required: true
         },
@@ -339,17 +357,10 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
   })
   Thumbnail: ThumbnailModel
 
-  static listForApi (options: {
-    followerActorId: number
+  static listForApi (options: AvailableForListOptions & {
     start: number
     count: number
     sort: string
-    type?: VideoPlaylistType
-    accountId?: number
-    videoChannelId?: number
-    listMyPlaylists?: boolean
-    search?: string
-    withVideos?: boolean // false by default
   }) {
     const query = {
       offset: options.start,
@@ -362,12 +373,8 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
         method: [
           ScopeNames.AVAILABLE_FOR_LIST,
           {
-            type: options.type,
-            followerActorId: options.followerActorId,
-            accountId: options.accountId,
-            videoChannelId: options.videoChannelId,
-            listMyPlaylists: options.listMyPlaylists,
-            search: options.search,
+            ...pick(options, [ 'type', 'followerActorId', 'accountId', 'videoChannelId', 'listMyPlaylists', 'search', 'host', 'uuids' ]),
+
             withVideos: options.withVideos || false
           } as AvailableForListOptions
         ]
@@ -384,15 +391,14 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
       })
   }
 
-  static searchForApi (options: {
-    followerActorId: number
+  static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & {
     start: number
     count: number
     sort: string
-    search?: string
   }) {
     return VideoPlaylistModel.listForApi({
       ...options,
+
       type: VideoPlaylistType.REGULAR,
       listMyPlaylists: false,
       withVideos: true
@@ -560,12 +566,12 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
     return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
   }
 
-  getWatchUrl () {
-    return WEBSERVER.URL + '/w/p/' + this.uuid
+  getWatchStaticPath () {
+    return buildPlaylistWatchPath({ shortUUID: uuidToShort(this.uuid) })
   }
 
   getEmbedStaticPath () {
-    return '/video-playlists/embed/' + this.uuid
+    return buildPlaylistEmbedPath(this)
   }
 
   static async getStats () {
index d627e8c9da9b180683e8a7b81777e0a546c0f544..d591a3134f08df6d133fc011ad261b492f8aed79 100644 (file)
@@ -1,19 +1,27 @@
 import * as memoizee from 'memoizee'
 import { join } from 'path'
-import { Op, QueryTypes } from 'sequelize'
+import { Op } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { VideoFileModel } from '@server/models/video/video-file'
-import { MStreamingPlaylist } from '@server/types/models'
+import { MStreamingPlaylist, MVideo } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
 import { sha1 } from '../../helpers/core-utils'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { isArrayOf } from '../../helpers/custom-validators/misc'
 import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
-import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants'
+import {
+  CONSTRAINTS_FIELDS,
+  MEMOIZE_LENGTH,
+  MEMOIZE_TTL,
+  P2P_MEDIA_LOADER_PEER_VERSION,
+  STATIC_PATHS,
+  WEBSERVER
+} from '../../initializers/constants'
 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
+import { doesExist } from '../shared'
 import { throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
-import { AttributesOnly } from '@shared/core-utils'
 
 @Table({
   tableName: 'videoStreamingPlaylist',
@@ -43,7 +51,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
   type: VideoStreamingPlaylistType
 
   @AllowNull(false)
-  @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url'))
+  @Column
+  playlistFilename: string
+
+  @AllowNull(true)
+  @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url', true))
   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
   playlistUrl: string
 
@@ -57,7 +69,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
   p2pMediaLoaderPeerVersion: number
 
   @AllowNull(false)
-  @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url'))
+  @Column
+  segmentsSha256Filename: string
+
+  @AllowNull(true)
+  @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url', true))
   @Column
   segmentsSha256Url: string
 
@@ -98,14 +114,8 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
 
   static doesInfohashExist (infoHash: string) {
     const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1'
-    const options = {
-      type: QueryTypes.SELECT as QueryTypes.SELECT,
-      bind: { infoHash },
-      raw: true
-    }
 
-    return VideoModel.sequelize.query<object>(query, options)
-              .then(results => results.length === 1)
+    return doesExist(query, { infoHash })
   }
 
   static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
@@ -125,7 +135,13 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
         p2pMediaLoaderPeerVersion: {
           [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION
         }
-      }
+      },
+      include: [
+        {
+          model: VideoModel.unscoped(),
+          required: true
+        }
+      ]
     }
 
     return VideoStreamingPlaylistModel.findAll(query)
@@ -144,7 +160,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
     return VideoStreamingPlaylistModel.findByPk(id, options)
   }
 
-  static loadHLSPlaylistByVideo (videoId: number) {
+  static loadHLSPlaylistByVideo (videoId: number): Promise<MStreamingPlaylist> {
     const options = {
       where: {
         type: VideoStreamingPlaylistType.HLS,
@@ -155,30 +171,29 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
     return VideoStreamingPlaylistModel.findOne(options)
   }
 
-  static getHlsPlaylistFilename (resolution: number) {
-    return resolution + '.m3u8'
-  }
+  static async loadOrGenerate (video: MVideo) {
+    let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id)
+    if (!playlist) playlist = new VideoStreamingPlaylistModel()
 
-  static getMasterHlsPlaylistFilename () {
-    return 'master.m3u8'
+    return Object.assign(playlist, { videoId: video.id, Video: video })
   }
 
-  static getHlsSha256SegmentsFilename () {
-    return 'segments-sha256.json'
-  }
+  assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) {
+    const masterPlaylistUrl = this.getMasterPlaylistUrl(video)
 
-  static getHlsMasterPlaylistStaticPath (videoUUID: string) {
-    return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
+    this.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(masterPlaylistUrl, files)
   }
 
-  static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) {
-    return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
+  getMasterPlaylistUrl (video: MVideo) {
+    if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid)
+
+    return this.playlistUrl
   }
 
-  static getHlsSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) {
-    if (isLive) return join('/live', 'segments-sha256', videoUUID)
+  getSha256SegmentsUrl (video: MVideo) {
+    if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive)
 
-    return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
+    return this.segmentsSha256Url
   }
 
   getStringType () {
@@ -195,4 +210,14 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
     return this.type === other.type &&
       this.videoId === other.videoId
   }
+
+  private getMasterPlaylistStaticPath (videoUUID: string) {
+    return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename)
+  }
+
+  private getSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) {
+    if (isLive) return join('/live', 'segments-sha256', videoUUID)
+
+    return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.segmentsSha256Filename)
+  }
 }
index 1e5648a36cbe383177816b5135311d0c7966e31f..56a5b0e18ee0f9236529c7cbfdea0e98f82bbc17 100644 (file)
@@ -24,14 +24,14 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { setAsUpdated } from '@server/helpers/database-utils'
 import { buildNSFWFilter } from '@server/helpers/express-utils'
+import { uuidToShort } from '@server/helpers/uuid'
 import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
 import { LiveManager } from '@server/lib/live/live-manager'
 import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths'
 import { getServerActor } from '@server/models/application/application'
 import { ModelCache } from '@server/models/model-cache'
-import { AttributesOnly } from '@shared/core-utils'
+import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils'
 import { VideoFile } from '@shared/models/videos/video-file.model'
 import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
 import { VideoObject } from '../../../shared/models/activitypub/objects'
@@ -91,6 +91,7 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy'
 import { ServerModel } from '../server/server'
 import { TrackerModel } from '../server/tracker'
 import { VideoTrackerModel } from '../server/video-tracker'
+import { setAsUpdated } from '../shared'
 import { UserModel } from '../user/user'
 import { UserVideoHistoryModel } from '../user/user-video-history'
 import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
@@ -762,8 +763,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
 
       // Remove physical files and torrents
       instance.VideoFiles.forEach(file => {
-        tasks.push(instance.removeFile(file))
-        tasks.push(file.removeTorrent())
+        tasks.push(instance.removeFileAndTorrent(file))
       })
 
       // Remove playlists file
@@ -1070,7 +1070,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     const trendingDays = options.sort.endsWith('trending')
       ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
       : undefined
-    let trendingAlgorithm
+
+    let trendingAlgorithm: string
     if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot'
     if (options.sort.endsWith('best')) trendingAlgorithm = 'best'
 
@@ -1082,40 +1083,44 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
       : serverActor.id
 
     const queryOptions = {
-      start: options.start,
-      count: options.count,
-      sort: options.sort,
+      ...pick(options, [
+        'start',
+        'count',
+        'sort',
+        'nsfw',
+        'isLive',
+        'categoryOneOf',
+        'licenceOneOf',
+        'languageOneOf',
+        'tagsOneOf',
+        'tagsAllOf',
+        'filter',
+        'withFiles',
+        'accountId',
+        'videoChannelId',
+        'videoPlaylistId',
+        'includeLocalVideos',
+        'user',
+        'historyOfUser',
+        'search'
+      ]),
+
       followerActorId,
       serverAccountId: serverActor.Account.id,
-      nsfw: options.nsfw,
-      isLive: options.isLive,
-      categoryOneOf: options.categoryOneOf,
-      licenceOneOf: options.licenceOneOf,
-      languageOneOf: options.languageOneOf,
-      tagsOneOf: options.tagsOneOf,
-      tagsAllOf: options.tagsAllOf,
-      filter: options.filter,
-      withFiles: options.withFiles,
-      accountId: options.accountId,
-      videoChannelId: options.videoChannelId,
-      videoPlaylistId: options.videoPlaylistId,
-      includeLocalVideos: options.includeLocalVideos,
-      user: options.user,
-      historyOfUser: options.historyOfUser,
       trendingDays,
-      trendingAlgorithm,
-      search: options.search
+      trendingAlgorithm
     }
 
     return VideoModel.getAvailableForApi(queryOptions, options.countVideos)
   }
 
   static async searchAndPopulateAccountAndServer (options: {
+    start: number
+    count: number
+    sort: string
     includeLocalVideos: boolean
     search?: string
-    start?: number
-    count?: number
-    sort?: string
+    host?: string
     startDate?: string // ISO 8601
     endDate?: string // ISO 8601
     originallyPublishedStartDate?: string
@@ -1131,41 +1136,38 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     durationMax?: number // seconds
     user?: MUserAccountId
     filter?: VideoFilter
+    uuids?: string[]
   }) {
     const serverActor = await getServerActor()
 
     const queryOptions = {
-      followerActorId: serverActor.id,
-      serverAccountId: serverActor.Account.id,
-
-      includeLocalVideos: options.includeLocalVideos,
-      nsfw: options.nsfw,
-      isLive: options.isLive,
-
-      categoryOneOf: options.categoryOneOf,
-      licenceOneOf: options.licenceOneOf,
-      languageOneOf: options.languageOneOf,
+      ...pick(options, [
+        'includeLocalVideos',
+        'nsfw',
+        'isLive',
+        'categoryOneOf',
+        'licenceOneOf',
+        'languageOneOf',
+        'tagsOneOf',
+        'tagsAllOf',
+        'user',
+        'filter',
+        'host',
+        'start',
+        'count',
+        'sort',
+        'startDate',
+        'endDate',
+        'originallyPublishedStartDate',
+        'originallyPublishedEndDate',
+        'durationMin',
+        'durationMax',
+        'uuids',
+        'search'
+      ]),
 
-      tagsOneOf: options.tagsOneOf,
-      tagsAllOf: options.tagsAllOf,
-
-      user: options.user,
-      filter: options.filter,
-
-      start: options.start,
-      count: options.count,
-      sort: options.sort,
-
-      startDate: options.startDate,
-      endDate: options.endDate,
-
-      originallyPublishedStartDate: options.originallyPublishedStartDate,
-      originallyPublishedEndDate: options.originallyPublishedEndDate,
-
-      durationMin: options.durationMin,
-      durationMax: options.durationMax,
-
-      search: options.search
+      followerActorId: serverActor.id,
+      serverAccountId: serverActor.Account.id
     }
 
     return VideoModel.getAvailableForApi(queryOptions)
@@ -1579,11 +1581,11 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
   }
 
   getWatchStaticPath () {
-    return '/w/' + this.uuid
+    return buildVideoWatchPath({ shortUUID: uuidToShort(this.uuid) })
   }
 
   getEmbedStaticPath () {
-    return '/videos/embed/' + this.uuid
+    return buildVideoEmbedPath(this)
   }
 
   getMiniatureStaticPath () {
@@ -1670,10 +1672,13 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
                                        .concat(toAdd)
   }
 
-  removeFile (videoFile: MVideoFile, isRedundancy = false) {
+  removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) {
     const filePath = getVideoFilePath(this, videoFile, isRedundancy)
-    return remove(filePath)
-      .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
+
+    const promises: Promise<any>[] = [ remove(filePath) ]
+    if (!isRedundancy) promises.push(videoFile.removeTorrent())
+
+    return Promise.all(promises)
   }
 
   async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
index 75ef56ce3ae072aaa5d337f1067e69947d2d04a4..51cf6e599ebe586a4728b6a67ad08d8b1d398452 100644 (file)
@@ -4,24 +4,18 @@ import 'mocha'
 import * as chai from 'chai'
 import {
   cleanupTests,
-  closeAllSequelize,
-  deleteAll,
+  createMultipleServers,
   doubleFollow,
-  getCount,
-  selectQuery,
-  setVideoField,
-  updateQuery,
-  wait
-} from '../../../../shared/extra-utils'
-import { flushAndRunMultipleServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { addVideoCommentThread, getVideoCommentThreads } from '../../../../shared/extra-utils/videos/video-comments'
-import { getVideo, rateVideo, uploadVideoAndGetId } from '../../../../shared/extra-utils/videos/videos'
+  PeerTubeServer,
+  setAccessTokensToServers,
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test AP cleaner', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let videoUUID1: string
   let videoUUID2: string
   let videoUUID3: string
@@ -36,7 +30,7 @@ describe('Test AP cleaner', function () {
         videos: { cleanup_remote_interactions: true }
       }
     }
-    servers = await flushAndRunMultipleServers(3, config)
+    servers = await createMultipleServers(3, config)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -52,9 +46,9 @@ describe('Test AP cleaner', function () {
     // Create 1 comment per video
     // Update 1 remote URL and 1 local URL on
 
-    videoUUID1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'server 1' })).uuid
-    videoUUID2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'server 2' })).uuid
-    videoUUID3 = (await uploadVideoAndGetId({ server: servers[2], videoName: 'server 3' })).uuid
+    videoUUID1 = (await servers[0].videos.quickUpload({ name: 'server 1' })).uuid
+    videoUUID2 = (await servers[1].videos.quickUpload({ name: 'server 2' })).uuid
+    videoUUID3 = (await servers[2].videos.quickUpload({ name: 'server 3' })).uuid
 
     videoUUIDs = [ videoUUID1, videoUUID2, videoUUID3 ]
 
@@ -62,8 +56,8 @@ describe('Test AP cleaner', function () {
 
     for (const server of servers) {
       for (const uuid of videoUUIDs) {
-        await rateVideo(server.url, server.accessToken, uuid, 'like')
-        await addVideoCommentThread(server.url, server.accessToken, uuid, 'comment')
+        await server.videos.rate({ id: uuid, rating: 'like' })
+        await server.comments.createThread({ videoId: uuid, text: 'comment' })
       }
     }
 
@@ -73,9 +67,10 @@ describe('Test AP cleaner', function () {
   it('Should have the correct likes', async function () {
     for (const server of servers) {
       for (const uuid of videoUUIDs) {
-        const res = await getVideo(server.url, uuid)
-        expect(res.body.likes).to.equal(3)
-        expect(res.body.dislikes).to.equal(0)
+        const video = await server.videos.get({ id: uuid })
+
+        expect(video.likes).to.equal(3)
+        expect(video.dislikes).to.equal(0)
       }
     }
   })
@@ -83,9 +78,9 @@ describe('Test AP cleaner', function () {
   it('Should destroy server 3 internal likes and correctly clean them', async function () {
     this.timeout(20000)
 
-    await deleteAll(servers[2].internalServerNumber, 'accountVideoRate')
+    await servers[2].sql.deleteAll('accountVideoRate')
     for (const uuid of videoUUIDs) {
-      await setVideoField(servers[2].internalServerNumber, uuid, 'likes', '0')
+      await servers[2].sql.setVideoField(uuid, 'likes', '0')
     }
 
     await wait(5000)
@@ -93,16 +88,16 @@ describe('Test AP cleaner', function () {
 
     // Updated rates of my video
     {
-      const res = await getVideo(servers[0].url, videoUUID1)
-      expect(res.body.likes).to.equal(2)
-      expect(res.body.dislikes).to.equal(0)
+      const video = await servers[0].videos.get({ id: videoUUID1 })
+      expect(video.likes).to.equal(2)
+      expect(video.dislikes).to.equal(0)
     }
 
     // Did not update rates of a remote video
     {
-      const res = await getVideo(servers[0].url, videoUUID2)
-      expect(res.body.likes).to.equal(3)
-      expect(res.body.dislikes).to.equal(0)
+      const video = await servers[0].videos.get({ id: videoUUID2 })
+      expect(video.likes).to.equal(3)
+      expect(video.dislikes).to.equal(0)
     }
   })
 
@@ -111,7 +106,7 @@ describe('Test AP cleaner', function () {
 
     for (const server of servers) {
       for (const uuid of videoUUIDs) {
-        await rateVideo(server.url, server.accessToken, uuid, 'dislike')
+        await server.videos.rate({ id: uuid, rating: 'dislike' })
       }
     }
 
@@ -119,9 +114,9 @@ describe('Test AP cleaner', function () {
 
     for (const server of servers) {
       for (const uuid of videoUUIDs) {
-        const res = await getVideo(server.url, uuid)
-        expect(res.body.likes).to.equal(0)
-        expect(res.body.dislikes).to.equal(3)
+        const video = await server.videos.get({ id: uuid })
+        expect(video.likes).to.equal(0)
+        expect(video.dislikes).to.equal(3)
       }
     }
   })
@@ -129,10 +124,10 @@ describe('Test AP cleaner', function () {
   it('Should destroy server 3 internal dislikes and correctly clean them', async function () {
     this.timeout(20000)
 
-    await deleteAll(servers[2].internalServerNumber, 'accountVideoRate')
+    await servers[2].sql.deleteAll('accountVideoRate')
 
     for (const uuid of videoUUIDs) {
-      await setVideoField(servers[2].internalServerNumber, uuid, 'dislikes', '0')
+      await servers[2].sql.setVideoField(uuid, 'dislikes', '0')
     }
 
     await wait(5000)
@@ -140,31 +135,31 @@ describe('Test AP cleaner', function () {
 
     // Updated rates of my video
     {
-      const res = await getVideo(servers[0].url, videoUUID1)
-      expect(res.body.likes).to.equal(0)
-      expect(res.body.dislikes).to.equal(2)
+      const video = await servers[0].videos.get({ id: videoUUID1 })
+      expect(video.likes).to.equal(0)
+      expect(video.dislikes).to.equal(2)
     }
 
     // Did not update rates of a remote video
     {
-      const res = await getVideo(servers[0].url, videoUUID2)
-      expect(res.body.likes).to.equal(0)
-      expect(res.body.dislikes).to.equal(3)
+      const video = await servers[0].videos.get({ id: videoUUID2 })
+      expect(video.likes).to.equal(0)
+      expect(video.dislikes).to.equal(3)
     }
   })
 
   it('Should destroy server 3 internal shares and correctly clean them', async function () {
     this.timeout(20000)
 
-    const preCount = await getCount(servers[0].internalServerNumber, 'videoShare')
+    const preCount = await servers[0].sql.getCount('videoShare')
     expect(preCount).to.equal(6)
 
-    await deleteAll(servers[2].internalServerNumber, 'videoShare')
+    await servers[2].sql.deleteAll('videoShare')
     await wait(5000)
     await waitJobs(servers)
 
     // Still 6 because we don't have remote shares on local videos
-    const postCount = await getCount(servers[0].internalServerNumber, 'videoShare')
+    const postCount = await servers[0].sql.getCount('videoShare')
     expect(postCount).to.equal(6)
   })
 
@@ -172,18 +167,18 @@ describe('Test AP cleaner', function () {
     this.timeout(20000)
 
     {
-      const res = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 5)
-      expect(res.body.total).to.equal(3)
+      const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 })
+      expect(total).to.equal(3)
     }
 
-    await deleteAll(servers[2].internalServerNumber, 'videoComment')
+    await servers[2].sql.deleteAll('videoComment')
 
     await wait(5000)
     await waitJobs(servers)
 
     {
-      const res = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 5)
-      expect(res.body.total).to.equal(2)
+      const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 })
+      expect(total).to.equal(2)
     }
   })
 
@@ -193,7 +188,7 @@ describe('Test AP cleaner', function () {
     async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') {
       const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` +
         `INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'`
-      const res = await selectQuery(servers[0].internalServerNumber, query)
+      const res = await servers[0].sql.selectQuery(query)
 
       for (const rate of res) {
         const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`)
@@ -222,7 +217,7 @@ describe('Test AP cleaner', function () {
 
     {
       const query = `UPDATE "accountVideoRate" SET url = url || 'stan'`
-      await updateQuery(servers[1].internalServerNumber, query)
+      await servers[1].sql.updateQuery(query)
 
       await wait(5000)
       await waitJobs(servers)
@@ -239,7 +234,7 @@ describe('Test AP cleaner', function () {
       const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` +
         `INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'`
 
-      const res = await selectQuery(servers[0].internalServerNumber, query)
+      const res = await servers[0].sql.selectQuery(query)
 
       for (const comment of res) {
         const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`)
@@ -265,7 +260,7 @@ describe('Test AP cleaner', function () {
 
     {
       const query = `UPDATE "videoComment" SET url = url || 'kyle'`
-      await updateQuery(servers[1].internalServerNumber, query)
+      await servers[1].sql.updateQuery(query)
 
       await wait(5000)
       await waitJobs(servers)
@@ -277,7 +272,5 @@ describe('Test AP cleaner', function () {
 
   after(async function () {
     await cleanupTests(servers)
-
-    await closeAllSequelize(servers)
   })
 })
index be94e219c5e9738a6cab89d4d402594f916746c6..c3e4b7f74a26f866d261bbb243a8d1e6c3b878f8 100644 (file)
@@ -2,24 +2,21 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { VideoPlaylistPrivacy } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   cleanupTests,
-  createVideoPlaylist,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
   makeActivityPubGetRequest,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  setDefaultVideoChannel,
-  uploadVideoAndGetId
-} from '../../../../shared/extra-utils'
+  setDefaultVideoChannel
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test activitypub', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let video: { id: number, uuid: string, shortUUID: string }
   let playlist: { id: number, uuid: string, shortUUID: string }
 
@@ -64,19 +61,18 @@ describe('Test activitypub', function () {
   before(async function () {
     this.timeout(30000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
 
     {
-      video = await uploadVideoAndGetId({ server: servers[0], videoName: 'video' })
+      video = await servers[0].videos.quickUpload({ name: 'video' })
     }
 
     {
-      const playlistAttrs = { displayName: 'playlist', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[0].videoChannel.id }
-      const resCreate = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs })
-      playlist = resCreate.body.videoPlaylist
+      const attributes = { displayName: 'playlist', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[0].store.channel.id }
+      playlist = await servers[0].playlists.create({ attributes })
     }
 
     await doubleFollow(servers[0], servers[1])
index 35fd94eed6b66cd4181c47badc6fb2e8a03d3f18..422a75d6e1814ec802f66694c6d59ae878ad7975 100644 (file)
@@ -1,61 +1,44 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
-import {
-  cleanupTests,
-  closeAllSequelize,
-  createUser,
-  doubleFollow,
-  flushAndRunMultipleServers,
-  getVideosListSort,
-  ServerInfo,
-  setAccessTokensToServers,
-  setActorField,
-  setVideoField,
-  uploadVideo,
-  userLogin,
-  waitJobs
-} from '../../../../shared/extra-utils'
 import * as chai from 'chai'
-import { Video } from '../../../../shared/models/videos'
+import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test ActivityPub fetcher', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
 
     const user = { username: 'user1', password: 'password' }
     for (const server of servers) {
-      await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
+      await server.users.create({ username: user.username, password: user.password })
     }
 
-    const userAccessToken = await userLogin(servers[0], user)
+    const userAccessToken = await servers[0].login.getAccessToken(user)
 
-    await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video root' })
-    const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'bad video root' })
-    const badVideoUUID = res.body.video.uuid
-    await uploadVideo(servers[0].url, userAccessToken, { name: 'video user' })
+    await servers[0].videos.upload({ attributes: { name: 'video root' } })
+    const { uuid } = await servers[0].videos.upload({ attributes: { name: 'bad video root' } })
+    await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'video user' } })
 
     {
       const to = 'http://localhost:' + servers[0].port + '/accounts/user1'
       const value = 'http://localhost:' + servers[1].port + '/accounts/user1'
-      await setActorField(servers[0].internalServerNumber, to, 'url', value)
+      await servers[0].sql.setActorField(to, 'url', value)
     }
 
     {
-      const value = 'http://localhost:' + servers[2].port + '/videos/watch/' + badVideoUUID
-      await setVideoField(servers[0].internalServerNumber, badVideoUUID, 'url', value)
+      const value = 'http://localhost:' + servers[2].port + '/videos/watch/' + uuid
+      await servers[0].sql.setVideoField(uuid, 'url', value)
     }
   })
 
@@ -66,20 +49,18 @@ describe('Test ActivityPub fetcher', function () {
     await waitJobs(servers)
 
     {
-      const res = await getVideosListSort(servers[0].url, 'createdAt')
-      expect(res.body.total).to.equal(3)
+      const { total, data } = await servers[0].videos.list({ sort: 'createdAt' })
 
-      const data: Video[] = res.body.data
+      expect(total).to.equal(3)
       expect(data[0].name).to.equal('video root')
       expect(data[1].name).to.equal('bad video root')
       expect(data[2].name).to.equal('video user')
     }
 
     {
-      const res = await getVideosListSort(servers[1].url, 'createdAt')
-      expect(res.body.total).to.equal(1)
+      const { total, data } = await servers[1].videos.list({ sort: 'createdAt' })
 
-      const data: Video[] = res.body.data
+      expect(total).to.equal(1)
       expect(data[0].name).to.equal('video root')
     }
   })
@@ -88,7 +69,5 @@ describe('Test ActivityPub fetcher', function () {
     this.timeout(20000)
 
     await cleanupTests(servers)
-
-    await closeAllSequelize(servers)
   })
 })
index 66d7631b7213b0c9454525da54996bf562c9fb47..57b1cab23c21d3ebd5308d83c9f0c4d865f6a232 100644 (file)
@@ -2,11 +2,10 @@
 
 import 'mocha'
 import { expect } from 'chai'
-import { buildRequestStub } from '../../../../shared/extra-utils/miscs/stubs'
-import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto'
 import { cloneDeep } from 'lodash'
+import { buildAbsoluteFixturePath, buildRequestStub } from '@shared/extra-utils'
 import { buildSignedActivity } from '../../../helpers/activitypub'
-import { buildAbsoluteFixturePath } from '@shared/extra-utils'
+import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto'
 
 describe('Test activity pub helpers', function () {
   describe('When checking the Linked Signature', function () {
index c717f1a307676ce0b0fecd90b43dc9d3aa24df2d..81fee004471c2a348884f7271ecce2385443c8c6 100644 (file)
@@ -2,32 +2,20 @@
 
 import 'mocha'
 import {
-  cleanupTests, closeAllSequelize,
-  createVideoPlaylist,
+  cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  generateUserAccessToken,
-  getVideo,
-  getVideoPlaylist,
   killallServers,
-  reRunServer,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  setActorField,
   setDefaultVideoChannel,
-  setPlaylistField,
-  setVideoField,
-  uploadVideo,
-  uploadVideoAndGetId,
   wait,
   waitJobs
-} from '../../../../shared/extra-utils'
-import { getAccount } from '../../../../shared/extra-utils/users/accounts'
-import { VideoPlaylistPrivacy } from '../../../../shared/models/videos'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models'
 
 describe('Test AP refresher', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let videoUUID1: string
   let videoUUID2: string
   let videoUUID3: string
@@ -37,36 +25,36 @@ describe('Test AP refresher', function () {
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: false } })
+    servers = await createMultipleServers(2, { transcoding: { enabled: false } })
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
 
     {
-      videoUUID1 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video1' })).uuid
-      videoUUID2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video2' })).uuid
-      videoUUID3 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video3' })).uuid
+      videoUUID1 = (await servers[1].videos.quickUpload({ name: 'video1' })).uuid
+      videoUUID2 = (await servers[1].videos.quickUpload({ name: 'video2' })).uuid
+      videoUUID3 = (await servers[1].videos.quickUpload({ name: 'video3' })).uuid
     }
 
     {
-      const a1 = await generateUserAccessToken(servers[1], 'user1')
-      await uploadVideo(servers[1].url, a1, { name: 'video4' })
+      const token1 = await servers[1].users.generateUserAndToken('user1')
+      await servers[1].videos.upload({ token: token1, attributes: { name: 'video4' } })
 
-      const a2 = await generateUserAccessToken(servers[1], 'user2')
-      await uploadVideo(servers[1].url, a2, { name: 'video5' })
+      const token2 = await servers[1].users.generateUserAndToken('user2')
+      await servers[1].videos.upload({ token: token2, attributes: { name: 'video5' } })
     }
 
     {
-      const playlistAttrs = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].videoChannel.id }
-      const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs })
-      playlistUUID1 = res.body.videoPlaylist.uuid
+      const attributes = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].store.channel.id }
+      const created = await servers[1].playlists.create({ attributes })
+      playlistUUID1 = created.uuid
     }
 
     {
-      const playlistAttrs = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].videoChannel.id }
-      const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs })
-      playlistUUID2 = res.body.videoPlaylist.uuid
+      const attributes = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].store.channel.id }
+      const created = await servers[1].playlists.create({ attributes })
+      playlistUUID2 = created.uuid
     }
 
     await doubleFollow(servers[0], servers[1])
@@ -80,34 +68,34 @@ describe('Test AP refresher', function () {
       await wait(10000)
 
       // Change UUID so the remote server returns a 404
-      await setVideoField(servers[1].internalServerNumber, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
+      await servers[1].sql.setVideoField(videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
 
-      await getVideo(servers[0].url, videoUUID1)
-      await getVideo(servers[0].url, videoUUID2)
+      await servers[0].videos.get({ id: videoUUID1 })
+      await servers[0].videos.get({ id: videoUUID2 })
 
       await waitJobs(servers)
 
-      await getVideo(servers[0].url, videoUUID1, HttpStatusCode.NOT_FOUND_404)
-      await getVideo(servers[0].url, videoUUID2, HttpStatusCode.OK_200)
+      await servers[0].videos.get({ id: videoUUID1, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      await servers[0].videos.get({ id: videoUUID2 })
     })
 
     it('Should not update a remote video if the remote instance is down', async function () {
       this.timeout(70000)
 
-      killallServers([ servers[1] ])
+      await killallServers([ servers[1] ])
 
-      await setVideoField(servers[1].internalServerNumber, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
+      await servers[1].sql.setVideoField(videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
 
       // Video will need a refresh
       await wait(10000)
 
-      await getVideo(servers[0].url, videoUUID3)
+      await servers[0].videos.get({ id: videoUUID3 })
       // The refresh should fail
       await waitJobs([ servers[0] ])
 
-      await reRunServer(servers[1])
+      await servers[1].run()
 
-      await getVideo(servers[0].url, videoUUID3, HttpStatusCode.OK_200)
+      await servers[0].videos.get({ id: videoUUID3 })
     })
   })
 
@@ -116,19 +104,21 @@ describe('Test AP refresher', function () {
     it('Should remove a deleted actor', async function () {
       this.timeout(60000)
 
+      const command = servers[0].accounts
+
       await wait(10000)
 
       // Change actor name so the remote server returns a 404
       const to = 'http://localhost:' + servers[1].port + '/accounts/user2'
-      await setActorField(servers[1].internalServerNumber, to, 'preferredUsername', 'toto')
+      await servers[1].sql.setActorField(to, 'preferredUsername', 'toto')
 
-      await getAccount(servers[0].url, 'user1@localhost:' + servers[1].port)
-      await getAccount(servers[0].url, 'user2@localhost:' + servers[1].port)
+      await command.get({ accountName: 'user1@localhost:' + servers[1].port })
+      await command.get({ accountName: 'user2@localhost:' + servers[1].port })
 
       await waitJobs(servers)
 
-      await getAccount(servers[0].url, 'user1@localhost:' + servers[1].port, HttpStatusCode.OK_200)
-      await getAccount(servers[0].url, 'user2@localhost:' + servers[1].port, HttpStatusCode.NOT_FOUND_404)
+      await command.get({ accountName: 'user1@localhost:' + servers[1].port, expectedStatus: HttpStatusCode.OK_200 })
+      await command.get({ accountName: 'user2@localhost:' + servers[1].port, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
   })
 
@@ -140,15 +130,15 @@ describe('Test AP refresher', function () {
       await wait(10000)
 
       // Change UUID so the remote server returns a 404
-      await setPlaylistField(servers[1].internalServerNumber, playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
+      await servers[1].sql.setPlaylistField(playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
 
-      await getVideoPlaylist(servers[0].url, playlistUUID1)
-      await getVideoPlaylist(servers[0].url, playlistUUID2)
+      await servers[0].playlists.get({ playlistId: playlistUUID1 })
+      await servers[0].playlists.get({ playlistId: playlistUUID2 })
 
       await waitJobs(servers)
 
-      await getVideoPlaylist(servers[0].url, playlistUUID1, HttpStatusCode.OK_200)
-      await getVideoPlaylist(servers[0].url, playlistUUID2, HttpStatusCode.NOT_FOUND_404)
+      await servers[0].playlists.get({ playlistId: playlistUUID1, expectedStatus: HttpStatusCode.OK_200 })
+      await servers[0].playlists.get({ playlistId: playlistUUID2, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
   })
 
@@ -156,7 +146,5 @@ describe('Test AP refresher', function () {
     this.timeout(10000)
 
     await cleanupTests(servers)
-
-    await closeAllSequelize(servers)
   })
 })
index 61db272f634ff36981439558336c9e32425efb43..94d946563c2d62631110a3e17e412c840413bb89 100644 (file)
@@ -2,45 +2,35 @@
 
 import 'mocha'
 import * as chai from 'chai'
+import { activityPubContextify, buildSignedActivity } from '@server/helpers/activitypub'
 import { buildDigest } from '@server/helpers/peertube-crypto'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import {
-  buildAbsoluteFixturePath,
-  cleanupTests,
-  closeAllSequelize,
-  flushAndRunMultipleServers,
-  killallServers,
-  reRunServer,
-  ServerInfo,
-  setActorField,
-  wait
-} from '../../../../shared/extra-utils'
-import { makeFollowRequest, makePOSTAPRequest } from '../../../../shared/extra-utils/requests/activitypub'
-import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
-import { HTTP_SIGNATURE } from '../../../initializers/constants'
-import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
+import { HTTP_SIGNATURE } from '@server/initializers/constants'
+import { buildGlobalHeaders } from '@server/lib/job-queue/handlers/utils/activitypub-http-utils'
+import { buildAbsoluteFixturePath, cleanupTests, createMultipleServers, killallServers, PeerTubeServer, wait } from '@shared/extra-utils'
+import { makeFollowRequest, makePOSTAPRequest } from '@shared/extra-utils/requests/activitypub'
+import { HttpStatusCode } from '@shared/models'
 
 const expect = chai.expect
 
-function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) {
+function setKeysOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, publicKey: string, privateKey: string) {
   const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
 
   return Promise.all([
-    setActorField(onServer.internalServerNumber, url, 'publicKey', publicKey),
-    setActorField(onServer.internalServerNumber, url, 'privateKey', privateKey)
+    onServer.sql.setActorField(url, 'publicKey', publicKey),
+    onServer.sql.setActorField(url, 'privateKey', privateKey)
   ])
 }
 
-function setUpdatedAtOfServer (onServer: ServerInfo, ofServer: ServerInfo, updatedAt: string) {
+function setUpdatedAtOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, updatedAt: string) {
   const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
 
   return Promise.all([
-    setActorField(onServer.internalServerNumber, url, 'createdAt', updatedAt),
-    setActorField(onServer.internalServerNumber, url, 'updatedAt', updatedAt)
+    onServer.sql.setActorField(url, 'createdAt', updatedAt),
+    onServer.sql.setActorField(url, 'updatedAt', updatedAt)
   ])
 }
 
-function getAnnounceWithoutContext (server: ServerInfo) {
+function getAnnounceWithoutContext (server: PeerTubeServer) {
   const json = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
   const result: typeof json = {}
 
@@ -56,7 +46,7 @@ function getAnnounceWithoutContext (server: ServerInfo) {
 }
 
 describe('Test ActivityPub security', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let url: string
 
   const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
@@ -74,7 +64,7 @@ describe('Test ActivityPub security', function () {
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
 
     url = servers[0].url + '/inbox'
 
@@ -173,8 +163,8 @@ describe('Test ActivityPub security', function () {
       await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')
 
       // Invalid peertube actor cache
-      killallServers([ servers[1] ])
-      await reRunServer(servers[1])
+      await killallServers([ servers[1] ])
+      await servers[1].run()
 
       const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
       const headers = buildGlobalHeaders(body)
@@ -294,7 +284,5 @@ describe('Test ActivityPub security', function () {
     this.timeout(10000)
 
     await cleanupTests(servers)
-
-    await closeAllSequelize(servers)
   })
 })
index 2054776cc62c9921d6952982442fdcc791b6b50e..fb9a5fd8b061b05a7db52dc35cfa9b58edebbd75 100644 (file)
@@ -1,66 +1,49 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { AbuseCreate, AbuseState } from '@shared/models'
 import {
-  addAbuseMessage,
+  AbusesCommand,
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   cleanupTests,
-  createUser,
-  deleteAbuse,
-  deleteAbuseMessage,
+  createSingleServer,
   doubleFollow,
-  flushAndRunServer,
-  generateUserAccessToken,
-  getAdminAbusesList,
-  getVideoIdFromUUID,
-  listAbuseMessages,
   makeGetRequest,
   makePostBodyRequest,
-  reportAbuse,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updateAbuse,
-  uploadVideo,
-  userLogin,
   waitJobs
-} from '../../../../shared/extra-utils'
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+} from '@shared/extra-utils'
+import { AbuseCreate, AbuseState, HttpStatusCode } from '@shared/models'
 
 describe('Test abuses API validators', function () {
   const basePath = '/api/v1/abuses/'
 
-  let server: ServerInfo
+  let server: PeerTubeServer
 
-  let userAccessToken = ''
-  let userAccessToken2 = ''
+  let userToken = ''
+  let userToken2 = ''
   let abuseId: number
   let messageId: number
 
+  let command: AbusesCommand
+
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
-    const username = 'user1'
-    const password = 'my super password'
-    await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
-    userAccessToken = await userLogin(server, { username, password })
+    userToken = await server.users.generateUserAndToken('user_1')
+    userToken2 = await server.users.generateUserAndToken('user_2')
 
-    {
-      userAccessToken2 = await generateUserAccessToken(server, 'user_2')
-    }
+    server.store.videoCreated = await server.videos.upload()
 
-    const res = await uploadVideo(server.url, server.accessToken, {})
-    server.video = res.body.video
+    command = server.abuses
   })
 
   describe('When listing abuses for admins', function () {
@@ -82,7 +65,7 @@ describe('Test abuses API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -90,8 +73,8 @@ describe('Test abuses API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        token: userToken,
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -126,7 +109,7 @@ describe('Test abuses API validators', function () {
         videoIs: 'deleted'
       }
 
-      await makeGetRequest({ url: server.url, path, token: server.accessToken, query, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, path, token: server.accessToken, query, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -134,32 +117,32 @@ describe('Test abuses API validators', function () {
     const path = '/api/v1/users/me/abuses'
 
     it('Should fail with a bad start pagination', async function () {
-      await checkBadStartPagination(server.url, path, userAccessToken)
+      await checkBadStartPagination(server.url, path, userToken)
     })
 
     it('Should fail with a bad count pagination', async function () {
-      await checkBadCountPagination(server.url, path, userAccessToken)
+      await checkBadCountPagination(server.url, path, userToken)
     })
 
     it('Should fail with an incorrect sort', async function () {
-      await checkBadSortPagination(server.url, path, userAccessToken)
+      await checkBadSortPagination(server.url, path, userToken)
     })
 
     it('Should fail with a non authenticated user', async function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
     it('Should fail with a bad id filter', async function () {
-      await makeGetRequest({ url: server.url, path, token: userAccessToken, query: { id: 'toto' } })
+      await makeGetRequest({ url: server.url, path, token: userToken, query: { id: 'toto' } })
     })
 
     it('Should fail with a bad state filter', async function () {
-      await makeGetRequest({ url: server.url, path, token: userAccessToken, query: { state: 'toto' } })
-      await makeGetRequest({ url: server.url, path, token: userAccessToken, query: { state: 0 } })
+      await makeGetRequest({ url: server.url, path, token: userToken, query: { state: 'toto' } })
+      await makeGetRequest({ url: server.url, path, token: userToken, query: { state: 0 } })
     })
 
     it('Should succeed with the correct params', async function () {
@@ -168,7 +151,7 @@ describe('Test abuses API validators', function () {
         state: 2
       }
 
-      await makeGetRequest({ url: server.url, path, token: userAccessToken, query, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, path, token: userToken, query, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -177,12 +160,12 @@ describe('Test abuses API validators', function () {
 
     it('Should fail with nothing', async function () {
       const fields = {}
-      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
     })
 
     it('Should fail with a wrong video', async function () {
       const fields = { video: { id: 'blabla' }, reason: 'my super reason' }
-      await makePostBodyRequest({ url: server.url, path: path, token: userAccessToken, fields })
+      await makePostBodyRequest({ url: server.url, path: path, token: userToken, fields })
     })
 
     it('Should fail with an unknown video', async function () {
@@ -190,15 +173,15 @@ describe('Test abuses API validators', function () {
       await makePostBodyRequest({
         url: server.url,
         path,
-        token: userAccessToken,
+        token: userToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
     it('Should fail with a wrong comment', async function () {
       const fields = { comment: { id: 'blabla' }, reason: 'my super reason' }
-      await makePostBodyRequest({ url: server.url, path: path, token: userAccessToken, fields })
+      await makePostBodyRequest({ url: server.url, path: path, token: userToken, fields })
     })
 
     it('Should fail with an unknown comment', async function () {
@@ -206,15 +189,15 @@ describe('Test abuses API validators', function () {
       await makePostBodyRequest({
         url: server.url,
         path,
-        token: userAccessToken,
+        token: userToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
     it('Should fail with a wrong account', async function () {
       const fields = { account: { id: 'blabla' }, reason: 'my super reason' }
-      await makePostBodyRequest({ url: server.url, path: path, token: userAccessToken, fields })
+      await makePostBodyRequest({ url: server.url, path: path, token: userToken, fields })
     })
 
     it('Should fail with an unknown account', async function () {
@@ -222,9 +205,9 @@ describe('Test abuses API validators', function () {
       await makePostBodyRequest({
         url: server.url,
         path,
-        token: userAccessToken,
+        token: userToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -233,65 +216,65 @@ describe('Test abuses API validators', function () {
       await makePostBodyRequest({
         url: server.url,
         path,
-        token: userAccessToken,
+        token: userToken,
         fields,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
     it('Should fail with a non authenticated user', async function () {
-      const fields = { video: { id: server.video.id }, reason: 'my super reason' }
+      const fields = { video: { id: server.store.videoCreated.id }, reason: 'my super reason' }
 
-      await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a reason too short', async function () {
-      const fields = { video: { id: server.video.id }, reason: 'h' }
+      const fields = { video: { id: server.store.videoCreated.id }, reason: 'h' }
 
-      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
     })
 
     it('Should fail with a too big reason', async function () {
-      const fields = { video: { id: server.video.id }, reason: 'super'.repeat(605) }
+      const fields = { video: { id: server.store.videoCreated.id }, reason: 'super'.repeat(605) }
 
-      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
     })
 
     it('Should succeed with the correct parameters (basic)', async function () {
-      const fields: AbuseCreate = { video: { id: server.video.shortUUID }, reason: 'my super reason' }
+      const fields: AbuseCreate = { video: { id: server.store.videoCreated.shortUUID }, reason: 'my super reason' }
 
       const res = await makePostBodyRequest({
         url: server.url,
         path,
-        token: userAccessToken,
+        token: userToken,
         fields,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
       abuseId = res.body.abuse.id
     })
 
     it('Should fail with a wrong predefined reason', async function () {
-      const fields = { video: { id: server.video.id }, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] }
+      const fields = { video: server.store.videoCreated, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] }
 
-      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
     })
 
     it('Should fail with negative timestamps', async function () {
-      const fields = { video: { id: server.video.id, startAt: -1 }, reason: 'my super reason' }
+      const fields = { video: { id: server.store.videoCreated.id, startAt: -1 }, reason: 'my super reason' }
 
-      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
     })
 
     it('Should fail mith misordered startAt/endAt', async function () {
-      const fields = { video: { id: server.video.id, startAt: 5, endAt: 1 }, reason: 'my super reason' }
+      const fields = { video: { id: server.store.videoCreated.id, startAt: 5, endAt: 1 }, reason: 'my super reason' }
 
-      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
     })
 
     it('Should succeed with the corret parameters (advanced)', async function () {
       const fields: AbuseCreate = {
         video: {
-          id: server.video.id,
+          id: server.store.videoCreated.id,
           startAt: 1,
           endAt: 5
         },
@@ -299,37 +282,37 @@ describe('Test abuses API validators', function () {
         predefinedReasons: [ 'serverRules' ]
       }
 
-      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makePostBodyRequest({ url: server.url, path, token: userToken, fields, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
   describe('When updating an abuse', function () {
 
     it('Should fail with a non authenticated user', async function () {
-      await updateAbuse(server.url, 'blabla', abuseId, {}, HttpStatusCode.UNAUTHORIZED_401)
+      await command.update({ token: 'blabla', abuseId, body: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a non admin user', async function () {
-      await updateAbuse(server.url, userAccessToken, abuseId, {}, HttpStatusCode.FORBIDDEN_403)
+      await command.update({ token: userToken, abuseId, body: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with a bad abuse id', async function () {
-      await updateAbuse(server.url, server.accessToken, 45, {}, HttpStatusCode.NOT_FOUND_404)
+      await command.update({ abuseId: 45, body: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with a bad state', async function () {
       const body = { state: 5 }
-      await updateAbuse(server.url, server.accessToken, abuseId, body, HttpStatusCode.BAD_REQUEST_400)
+      await command.update({ abuseId, body, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should fail with a bad moderation comment', async function () {
       const body = { moderationComment: 'b'.repeat(3001) }
-      await updateAbuse(server.url, server.accessToken, abuseId, body, HttpStatusCode.BAD_REQUEST_400)
+      await command.update({ abuseId, body, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should succeed with the correct params', async function () {
       const body = { state: AbuseState.ACCEPTED }
-      await updateAbuse(server.url, server.accessToken, abuseId, body)
+      await command.update({ abuseId, body })
     })
   })
 
@@ -337,23 +320,23 @@ describe('Test abuses API validators', function () {
     const message = 'my super message'
 
     it('Should fail with an invalid abuse id', async function () {
-      await addAbuseMessage(server.url, userAccessToken2, 888, message, HttpStatusCode.NOT_FOUND_404)
+      await command.addMessage({ token: userToken2, abuseId: 888, message, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with a non authenticated user', async function () {
-      await addAbuseMessage(server.url, 'fake_token', abuseId, message, HttpStatusCode.UNAUTHORIZED_401)
+      await command.addMessage({ token: 'fake_token', abuseId, message, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with an invalid logged in user', async function () {
-      await addAbuseMessage(server.url, userAccessToken2, abuseId, message, HttpStatusCode.FORBIDDEN_403)
+      await command.addMessage({ token: userToken2, abuseId, message, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with an invalid message', async function () {
-      await addAbuseMessage(server.url, userAccessToken, abuseId, 'a'.repeat(5000), HttpStatusCode.BAD_REQUEST_400)
+      await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should suceed with the correct params', async function () {
-      const res = await addAbuseMessage(server.url, userAccessToken, abuseId, message)
+      const res = await command.addMessage({ token: userToken, abuseId, message })
       messageId = res.body.abuseMessage.id
     })
   })
@@ -361,96 +344,90 @@ describe('Test abuses API validators', function () {
   describe('When listing abuse messages', function () {
 
     it('Should fail with an invalid abuse id', async function () {
-      await listAbuseMessages(server.url, userAccessToken, 888, HttpStatusCode.NOT_FOUND_404)
+      await command.listMessages({ token: userToken, abuseId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with a non authenticated user', async function () {
-      await listAbuseMessages(server.url, 'fake_token', abuseId, HttpStatusCode.UNAUTHORIZED_401)
+      await command.listMessages({ token: 'fake_token', abuseId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with an invalid logged in user', async function () {
-      await listAbuseMessages(server.url, userAccessToken2, abuseId, HttpStatusCode.FORBIDDEN_403)
+      await command.listMessages({ token: userToken2, abuseId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should succeed with the correct params', async function () {
-      await listAbuseMessages(server.url, userAccessToken, abuseId)
+      await command.listMessages({ token: userToken, abuseId })
     })
   })
 
   describe('When deleting an abuse message', function () {
-
     it('Should fail with an invalid abuse id', async function () {
-      await deleteAbuseMessage(server.url, userAccessToken, 888, messageId, HttpStatusCode.NOT_FOUND_404)
+      await command.deleteMessage({ token: userToken, abuseId: 888, messageId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with an invalid message id', async function () {
-      await deleteAbuseMessage(server.url, userAccessToken, abuseId, 888, HttpStatusCode.NOT_FOUND_404)
+      await command.deleteMessage({ token: userToken, abuseId, messageId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with a non authenticated user', async function () {
-      await deleteAbuseMessage(server.url, 'fake_token', abuseId, messageId, HttpStatusCode.UNAUTHORIZED_401)
+      await command.deleteMessage({ token: 'fake_token', abuseId, messageId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with an invalid logged in user', async function () {
-      await deleteAbuseMessage(server.url, userAccessToken2, abuseId, messageId, HttpStatusCode.FORBIDDEN_403)
+      await command.deleteMessage({ token: userToken2, abuseId, messageId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should succeed with the correct params', async function () {
-      await deleteAbuseMessage(server.url, userAccessToken, abuseId, messageId)
+      await command.deleteMessage({ token: userToken, abuseId, messageId })
     })
   })
 
   describe('When deleting a video abuse', function () {
 
     it('Should fail with a non authenticated user', async function () {
-      await deleteAbuse(server.url, 'blabla', abuseId, HttpStatusCode.UNAUTHORIZED_401)
+      await command.delete({ token: 'blabla', abuseId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a non admin user', async function () {
-      await deleteAbuse(server.url, userAccessToken, abuseId, HttpStatusCode.FORBIDDEN_403)
+      await command.delete({ token: userToken, abuseId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with a bad abuse id', async function () {
-      await deleteAbuse(server.url, server.accessToken, 45, HttpStatusCode.NOT_FOUND_404)
+      await command.delete({ abuseId: 45, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should succeed with the correct params', async function () {
-      await deleteAbuse(server.url, server.accessToken, abuseId)
+      await command.delete({ abuseId })
     })
   })
 
   describe('When trying to manage messages of a remote abuse', function () {
     let remoteAbuseId: number
-    let anotherServer: ServerInfo
+    let anotherServer: PeerTubeServer
 
     before(async function () {
       this.timeout(50000)
 
-      anotherServer = await flushAndRunServer(2)
+      anotherServer = await createSingleServer(2)
       await setAccessTokensToServers([ anotherServer ])
 
       await doubleFollow(anotherServer, server)
 
-      const server2VideoId = await getVideoIdFromUUID(anotherServer.url, server.video.uuid)
-      await reportAbuse({
-        url: anotherServer.url,
-        token: anotherServer.accessToken,
-        reason: 'remote server',
-        videoId: server2VideoId
-      })
+      const server2VideoId = await anotherServer.videos.getId({ uuid: server.store.videoCreated.uuid })
+      await anotherServer.abuses.report({ reason: 'remote server', videoId: server2VideoId })
 
       await waitJobs([ server, anotherServer ])
 
-      const res = await getAdminAbusesList({ url: server.url, token: server.accessToken, sort: '-createdAt' })
-      remoteAbuseId = res.body.data[0].id
+      const body = await command.getAdminList({ sort: '-createdAt' })
+      remoteAbuseId = body.data[0].id
     })
 
     it('Should fail when listing abuse messages of a remote abuse', async function () {
-      await listAbuseMessages(server.url, server.accessToken, remoteAbuseId, HttpStatusCode.BAD_REQUEST_400)
+      await command.listMessages({ abuseId: remoteAbuseId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should fail when creating abuse message of a remote abuse', async function () {
-      await addAbuseMessage(server.url, server.accessToken, remoteAbuseId, 'message', HttpStatusCode.BAD_REQUEST_400)
+      await command.addMessage({ abuseId: remoteAbuseId, message: 'message', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     after(async function () {
index d1712cff61999ab6f19ee785bede7942b204d4de..141d869b764da0e2bb284fb9a58dc591b4b8715a 100644 (file)
@@ -1,26 +1,26 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../../shared/extra-utils'
 import {
   checkBadCountPagination,
   checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { getAccount } from '../../../../shared/extra-utils/users/accounts'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+  checkBadStartPagination,
+  cleanupTests,
+  createSingleServer,
+  PeerTubeServer
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test accounts API validators', function () {
   const path = '/api/v1/accounts/'
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
   })
 
   describe('When listing accounts', function () {
@@ -38,8 +38,9 @@ describe('Test accounts API validators', function () {
   })
 
   describe('When getting an account', function () {
+
     it('Should return 404 with a non existing name', async function () {
-      await getAccount(server.url, 'arfaze', HttpStatusCode.NOT_FOUND_404)
+      await server.accounts.get({ accountName: 'arfaze', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
   })
 
index 5ed8810ced05c1f2efbcf92c60e082b87f6a565b..7d5fae5cfb917dd27e43ff66ffe041af4baeeb84 100644 (file)
@@ -1,43 +1,38 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
 import {
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   cleanupTests,
-  createUser,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
   makeDeleteRequest,
   makeGetRequest,
   makePostBodyRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test blocklist API validators', function () {
-  let servers: ServerInfo[]
-  let server: ServerInfo
+  let servers: PeerTubeServer[]
+  let server: PeerTubeServer
   let userAccessToken: string
 
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
 
     server = servers[0]
 
     const user = { username: 'user1', password: 'password' }
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
+    await server.users.create({ username: user.username, password: user.password })
 
-    userAccessToken = await userLogin(server, user)
+    userAccessToken = await server.login.getAccessToken(user)
 
     await doubleFollow(servers[0], servers[1])
   })
@@ -54,7 +49,7 @@ describe('Test blocklist API validators', function () {
           await makeGetRequest({
             url: server.url,
             path,
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -77,7 +72,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path,
             fields: { accountName: 'user1' },
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -87,7 +82,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { accountName: 'user2' },
-            statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+            expectedStatus: HttpStatusCode.NOT_FOUND_404
           })
         })
 
@@ -97,7 +92,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { accountName: 'root' },
-            statusCodeExpected: HttpStatusCode.CONFLICT_409
+            expectedStatus: HttpStatusCode.CONFLICT_409
           })
         })
 
@@ -107,7 +102,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { accountName: 'user1' },
-            statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+            expectedStatus: HttpStatusCode.NO_CONTENT_204
           })
         })
       })
@@ -117,7 +112,7 @@ describe('Test blocklist API validators', function () {
           await makeDeleteRequest({
             url: server.url,
             path: path + '/user1',
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -126,7 +121,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path: path + '/user2',
             token: server.accessToken,
-            statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+            expectedStatus: HttpStatusCode.NOT_FOUND_404
           })
         })
 
@@ -135,7 +130,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path: path + '/user1',
             token: server.accessToken,
-            statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+            expectedStatus: HttpStatusCode.NO_CONTENT_204
           })
         })
       })
@@ -149,7 +144,7 @@ describe('Test blocklist API validators', function () {
           await makeGetRequest({
             url: server.url,
             path,
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -172,7 +167,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path,
             fields: { host: 'localhost:9002' },
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -182,7 +177,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { host: 'localhost:9003' },
-            statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+            expectedStatus: HttpStatusCode.NO_CONTENT_204
           })
         })
 
@@ -192,7 +187,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { host: 'localhost:' + server.port },
-            statusCodeExpected: HttpStatusCode.CONFLICT_409
+            expectedStatus: HttpStatusCode.CONFLICT_409
           })
         })
 
@@ -202,7 +197,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { host: 'localhost:' + servers[1].port },
-            statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+            expectedStatus: HttpStatusCode.NO_CONTENT_204
           })
         })
       })
@@ -212,7 +207,7 @@ describe('Test blocklist API validators', function () {
           await makeDeleteRequest({
             url: server.url,
             path: path + '/localhost:' + servers[1].port,
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -221,7 +216,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path: path + '/localhost:9004',
             token: server.accessToken,
-            statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+            expectedStatus: HttpStatusCode.NOT_FOUND_404
           })
         })
 
@@ -230,7 +225,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path: path + '/localhost:' + servers[1].port,
             token: server.accessToken,
-            statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+            expectedStatus: HttpStatusCode.NO_CONTENT_204
           })
         })
       })
@@ -247,7 +242,7 @@ describe('Test blocklist API validators', function () {
           await makeGetRequest({
             url: server.url,
             path,
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -256,7 +251,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             token: userAccessToken,
             path,
-            statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+            expectedStatus: HttpStatusCode.FORBIDDEN_403
           })
         })
 
@@ -279,7 +274,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path,
             fields: { accountName: 'user1' },
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -289,7 +284,7 @@ describe('Test blocklist API validators', function () {
             token: userAccessToken,
             path,
             fields: { accountName: 'user1' },
-            statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+            expectedStatus: HttpStatusCode.FORBIDDEN_403
           })
         })
 
@@ -299,7 +294,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { accountName: 'user2' },
-            statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+            expectedStatus: HttpStatusCode.NOT_FOUND_404
           })
         })
 
@@ -309,7 +304,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { accountName: 'root' },
-            statusCodeExpected: HttpStatusCode.CONFLICT_409
+            expectedStatus: HttpStatusCode.CONFLICT_409
           })
         })
 
@@ -319,7 +314,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { accountName: 'user1' },
-            statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+            expectedStatus: HttpStatusCode.NO_CONTENT_204
           })
         })
       })
@@ -329,7 +324,7 @@ describe('Test blocklist API validators', function () {
           await makeDeleteRequest({
             url: server.url,
             path: path + '/user1',
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -338,7 +333,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path: path + '/user1',
             token: userAccessToken,
-            statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+            expectedStatus: HttpStatusCode.FORBIDDEN_403
           })
         })
 
@@ -347,7 +342,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path: path + '/user2',
             token: server.accessToken,
-            statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+            expectedStatus: HttpStatusCode.NOT_FOUND_404
           })
         })
 
@@ -356,7 +351,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path: path + '/user1',
             token: server.accessToken,
-            statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+            expectedStatus: HttpStatusCode.NO_CONTENT_204
           })
         })
       })
@@ -370,7 +365,7 @@ describe('Test blocklist API validators', function () {
           await makeGetRequest({
             url: server.url,
             path,
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -379,7 +374,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             token: userAccessToken,
             path,
-            statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+            expectedStatus: HttpStatusCode.FORBIDDEN_403
           })
         })
 
@@ -402,7 +397,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path,
             fields: { host: 'localhost:' + servers[1].port },
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -412,7 +407,7 @@ describe('Test blocklist API validators', function () {
             token: userAccessToken,
             path,
             fields: { host: 'localhost:' + servers[1].port },
-            statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+            expectedStatus: HttpStatusCode.FORBIDDEN_403
           })
         })
 
@@ -422,7 +417,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { host: 'localhost:9003' },
-            statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+            expectedStatus: HttpStatusCode.NO_CONTENT_204
           })
         })
 
@@ -432,7 +427,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { host: 'localhost:' + server.port },
-            statusCodeExpected: HttpStatusCode.CONFLICT_409
+            expectedStatus: HttpStatusCode.CONFLICT_409
           })
         })
 
@@ -442,7 +437,7 @@ describe('Test blocklist API validators', function () {
             token: server.accessToken,
             path,
             fields: { host: 'localhost:' + servers[1].port },
-            statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+            expectedStatus: HttpStatusCode.NO_CONTENT_204
           })
         })
       })
@@ -452,7 +447,7 @@ describe('Test blocklist API validators', function () {
           await makeDeleteRequest({
             url: server.url,
             path: path + '/localhost:' + servers[1].port,
-            statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+            expectedStatus: HttpStatusCode.UNAUTHORIZED_401
           })
         })
 
@@ -461,7 +456,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path: path + '/localhost:' + servers[1].port,
             token: userAccessToken,
-            statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+            expectedStatus: HttpStatusCode.FORBIDDEN_403
           })
         })
 
@@ -470,7 +465,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path: path + '/localhost:9004',
             token: server.accessToken,
-            statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+            expectedStatus: HttpStatusCode.NOT_FOUND_404
           })
         })
 
@@ -479,7 +474,7 @@ describe('Test blocklist API validators', function () {
             url: server.url,
             path: path + '/localhost:' + servers[1].port,
             token: server.accessToken,
-            statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+            expectedStatus: HttpStatusCode.NO_CONTENT_204
           })
         })
       })
index 07b920ba72a7c24e35f577da744bb9f1d325940c..bc9d7784d7f094808a41f3e2174f4580241f05e0 100644 (file)
@@ -1,19 +1,11 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import {
-  cleanupTests,
-  createUser,
-  flushAndRunServer,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { makePostBodyRequest } from '../../../../shared/extra-utils/requests/requests'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { cleanupTests, createSingleServer, makePostBodyRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test bulk API validators', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken: string
 
   // ---------------------------------------------------------------
@@ -21,13 +13,13 @@ describe('Test bulk API validators', function () {
   before(async function () {
     this.timeout(120000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
     const user = { username: 'user1', password: 'password' }
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
+    await server.users.create({ username: user.username, password: user.password })
 
-    userAccessToken = await userLogin(server, user)
+    userAccessToken = await server.login.getAccessToken(user)
   })
 
   describe('When removing comments of', function () {
@@ -38,7 +30,7 @@ describe('Test bulk API validators', function () {
         url: server.url,
         path,
         fields: { accountName: 'user1', scope: 'my-videos' },
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -48,7 +40,7 @@ describe('Test bulk API validators', function () {
         token: server.accessToken,
         path,
         fields: { accountName: 'user2', scope: 'my-videos' },
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -58,7 +50,7 @@ describe('Test bulk API validators', function () {
         token: server.accessToken,
         path,
         fields: { accountName: 'user1', scope: 'my-videoss' },
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -68,7 +60,7 @@ describe('Test bulk API validators', function () {
         token: userAccessToken,
         path,
         fields: { accountName: 'user1', scope: 'instance' },
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -78,7 +70,7 @@ describe('Test bulk API validators', function () {
         token: server.accessToken,
         path,
         fields: { accountName: 'user1', scope: 'instance' },
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
index 9549070ef1b71b69524da96796b72d01ffa8e848..87cb2287e8bf16cd49fc74ac97044dacc36edd96 100644 (file)
@@ -1,26 +1,21 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import { omit } from 'lodash'
 import 'mocha'
-import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
-
+import { omit } from 'lodash'
 import {
   cleanupTests,
-  createUser,
-  flushAndRunServer,
-  immutableAssign,
+  createSingleServer,
   makeDeleteRequest,
   makeGetRequest,
   makePutBodyRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { CustomConfig, HttpStatusCode } from '@shared/models'
 
 describe('Test config API validators', function () {
   const path = '/api/v1/config/custom'
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken: string
   const updateParams: CustomConfig = {
     instance: {
@@ -201,7 +196,7 @@ describe('Test config API validators', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
@@ -209,8 +204,8 @@ describe('Test config API validators', function () {
       username: 'user1',
       password: 'password'
     }
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-    userAccessToken = await userLogin(server, user)
+    await server.users.create({ username: user.username, password: user.password })
+    userAccessToken = await server.login.getAccessToken(user)
   })
 
   describe('When getting the configuration', function () {
@@ -218,7 +213,7 @@ describe('Test config API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -227,7 +222,7 @@ describe('Test config API validators', function () {
         url: server.url,
         path,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
   })
@@ -238,7 +233,7 @@ describe('Test config API validators', function () {
         url: server.url,
         path,
         fields: updateParams,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -248,7 +243,7 @@ describe('Test config API validators', function () {
         path,
         fields: updateParams,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -260,47 +255,53 @@ describe('Test config API validators', function () {
         path,
         fields: newUpdateParams,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
     it('Should fail with a bad default NSFW policy', async function () {
-      const newUpdateParams = immutableAssign(updateParams, {
+      const newUpdateParams = {
+        ...updateParams,
+
         instance: {
           defaultNSFWPolicy: 'hello'
         }
-      })
+      }
 
       await makePutBodyRequest({
         url: server.url,
         path,
         fields: newUpdateParams,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
     it('Should fail if email disabled and signup requires email verification', async function () {
       // opposite scenario - success when enable enabled - covered via tests/api/users/user-verification.ts
-      const newUpdateParams = immutableAssign(updateParams, {
+      const newUpdateParams = {
+        ...updateParams,
+
         signup: {
           enabled: true,
           limit: 5,
           requiresEmailVerification: true
         }
-      })
+      }
 
       await makePutBodyRequest({
         url: server.url,
         path,
         fields: newUpdateParams,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
     it('Should fail with a disabled webtorrent & hls transcoding', async function () {
-      const newUpdateParams = immutableAssign(updateParams, {
+      const newUpdateParams = {
+        ...updateParams,
+
         transcoding: {
           hls: {
             enabled: false
@@ -309,14 +310,14 @@ describe('Test config API validators', function () {
             enabled: false
           }
         }
-      })
+      }
 
       await makePutBodyRequest({
         url: server.url,
         path,
         fields: newUpdateParams,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -326,7 +327,7 @@ describe('Test config API validators', function () {
         path,
         fields: updateParams,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -336,7 +337,7 @@ describe('Test config API validators', function () {
       await makeDeleteRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -345,7 +346,7 @@ describe('Test config API validators', function () {
         url: server.url,
         path,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
   })
index c7f9c1b476332aaa6e19b179e09c3bbe94758aec..1df9993da04d9aee29383d9dfb4e8b3bf06419a8 100644 (file)
@@ -1,14 +1,12 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
-import { cleanupTests, flushAndRunServer, immutableAssign, killallServers, reRunServer, ServerInfo } from '../../../../shared/extra-utils'
-import { sendContactForm } from '../../../../shared/extra-utils/server/contact-form'
-import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { cleanupTests, createSingleServer, killallServers, MockSmtpServer, PeerTubeServer } from '@shared/extra-utils'
+import { ContactFormCommand } from '@shared/extra-utils/server'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test contact form API validators', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   const emails: object[] = []
   const defaultBody = {
     fromName: 'super name',
@@ -17,6 +15,7 @@ describe('Test contact form API validators', function () {
     body: 'Hello, how are you?'
   }
   let emailPort: number
+  let command: ContactFormCommand
 
   // ---------------------------------------------------------------
 
@@ -26,86 +25,51 @@ describe('Test contact form API validators', function () {
     emailPort = await MockSmtpServer.Instance.collectEmails(emails)
 
     // Email is disabled
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
+    command = server.contactForm
   })
 
   it('Should not accept a contact form if emails are disabled', async function () {
-    await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: HttpStatusCode.CONFLICT_409 }))
+    await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 })
   })
 
   it('Should not accept a contact form if it is disabled in the configuration', async function () {
     this.timeout(10000)
 
-    killallServers([ server ])
+    await killallServers([ server ])
 
     // Contact form is disabled
-    await reRunServer(server, { smtp: { hostname: 'localhost', port: emailPort }, contact_form: { enabled: false } })
-    await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: HttpStatusCode.CONFLICT_409 }))
+    await server.run({ smtp: { hostname: 'localhost', port: emailPort }, contact_form: { enabled: false } })
+    await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 })
   })
 
   it('Should not accept a contact form if from email is invalid', async function () {
     this.timeout(10000)
 
-    killallServers([ server ])
+    await killallServers([ server ])
 
     // Email & contact form enabled
-    await reRunServer(server, { smtp: { hostname: 'localhost', port: emailPort } })
-
-    await sendContactForm(immutableAssign(defaultBody, {
-      url: server.url,
-      expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-      fromEmail: 'badEmail'
-    }))
-    await sendContactForm(immutableAssign(defaultBody, {
-      url: server.url,
-      expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-      fromEmail: 'badEmail@'
-    }))
-    await sendContactForm(immutableAssign(defaultBody, {
-      url: server.url,
-      expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-      fromEmail: undefined
-    }))
+    await server.run({ smtp: { hostname: 'localhost', port: emailPort } })
+
+    await command.send({ ...defaultBody, fromEmail: 'badEmail', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await command.send({ ...defaultBody, fromEmail: 'badEmail@', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await command.send({ ...defaultBody, fromEmail: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should not accept a contact form if from name is invalid', async function () {
-    await sendContactForm(immutableAssign(defaultBody, {
-      url: server.url,
-      expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-      fromName: 'name'.repeat(100)
-    }))
-    await sendContactForm(immutableAssign(defaultBody, {
-      url: server.url,
-      expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-      fromName: ''
-    }))
-    await sendContactForm(immutableAssign(defaultBody, {
-      url: server.url,
-      expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-      fromName: undefined
-    }))
+    await command.send({ ...defaultBody, fromName: 'name'.repeat(100), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await command.send({ ...defaultBody, fromName: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await command.send({ ...defaultBody, fromName: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should not accept a contact form if body is invalid', async function () {
-    await sendContactForm(immutableAssign(defaultBody, {
-      url: server.url,
-      expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-      body: 'body'.repeat(5000)
-    }))
-    await sendContactForm(immutableAssign(defaultBody, {
-      url: server.url,
-      expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-      body: 'a'
-    }))
-    await sendContactForm(immutableAssign(defaultBody, {
-      url: server.url,
-      expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-      body: undefined
-    }))
+    await command.send({ ...defaultBody, body: 'body'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await command.send({ ...defaultBody, body: 'a', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await command.send({ ...defaultBody, body: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should accept a contact form with the correct parameters', async function () {
-    await sendContactForm(immutableAssign(defaultBody, { url: server.url }))
+    await command.send(defaultBody)
   })
 
   after(async function () {
index 74ca3384cac584c85c3e9241dae58905abc94820..9fbbea31503a7061d001fe5c6c3a37a9fe5622bb 100644 (file)
@@ -1,21 +1,20 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   cleanupTests,
-  createUser,
-  flushAndRunServer,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { makeGetRequest, makePutBodyRequest } from '../../../../shared/extra-utils/requests/requests'
+  createSingleServer,
+  makeGetRequest,
+  makePutBodyRequest,
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test custom pages validators', function () {
   const path = '/api/v1/custom-pages/homepage/instance'
 
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken: string
 
   // ---------------------------------------------------------------
@@ -23,13 +22,13 @@ describe('Test custom pages validators', function () {
   before(async function () {
     this.timeout(120000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
     const user = { username: 'user1', password: 'password' }
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
+    await server.users.create({ username: user.username, password: user.password })
 
-    userAccessToken = await userLogin(server, user)
+    userAccessToken = await server.login.getAccessToken(user)
   })
 
   describe('When updating instance homepage', function () {
@@ -39,7 +38,7 @@ describe('Test custom pages validators', function () {
         url: server.url,
         path,
         fields: { content: 'super content' },
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -49,7 +48,7 @@ describe('Test custom pages validators', function () {
         path,
         token: userAccessToken,
         fields: { content: 'super content' },
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -59,7 +58,7 @@ describe('Test custom pages validators', function () {
         path,
         token: server.accessToken,
         fields: { content: 'super content' },
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -70,7 +69,7 @@ describe('Test custom pages validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
index 37bf0f99b700d7fbd9a1289a38406ae98fb97f08..a55786359ebf478d750f35317961435ffc8ab068 100644 (file)
@@ -1,21 +1,12 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
-import {
-  cleanupTests,
-  createUser,
-  flushAndRunServer,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { makeGetRequest } from '../../../../shared/extra-utils/requests/requests'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { cleanupTests, createSingleServer, makeGetRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test debug API validators', function () {
   const path = '/api/v1/server/debug'
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken = ''
 
   // ---------------------------------------------------------------
@@ -23,7 +14,7 @@ describe('Test debug API validators', function () {
   before(async function () {
     this.timeout(120000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
@@ -31,8 +22,8 @@ describe('Test debug API validators', function () {
       username: 'user1',
       password: 'my super password'
     }
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-    userAccessToken = await userLogin(server, user)
+    await server.users.create({ username: user.username, password: user.password })
+    userAccessToken = await server.login.getAccessToken(user)
   })
 
   describe('When getting debug endpoint', function () {
@@ -41,7 +32,7 @@ describe('Test debug API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -50,7 +41,7 @@ describe('Test debug API validators', function () {
         url: server.url,
         path,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -60,7 +51,7 @@ describe('Test debug API validators', function () {
         path,
         token: server.accessToken,
         query: { startDate: new Date().toISOString() },
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
index c03dd5c9c4bcd19047413fac42556b019333aca7..2bc9f6b968175d10a982ba15cc3456a1860b1961 100644 (file)
@@ -1,33 +1,29 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
-import {
-  cleanupTests,
-  createUser,
-  flushAndRunServer,
-  makeDeleteRequest, makeGetRequest,
-  makePostBodyRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
 import {
   checkBadCountPagination,
   checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+  checkBadStartPagination,
+  cleanupTests,
+  createSingleServer,
+  makeDeleteRequest,
+  makeGetRequest,
+  makePostBodyRequest,
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test server follows API validators', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
   })
@@ -36,64 +32,68 @@ describe('Test server follows API validators', function () {
     let userAccessToken = null
 
     before(async function () {
-      const user = {
-        username: 'user1',
-        password: 'password'
-      }
-
-      await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-      userAccessToken = await userLogin(server, user)
+      userAccessToken = await server.users.generateUserAndToken('user1')
     })
 
     describe('When adding follows', function () {
       const path = '/api/v1/server/following'
 
-      it('Should fail without hosts', async function () {
+      it('Should fail with nothing', async function () {
         await makePostBodyRequest({
           url: server.url,
           path,
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       })
 
-      it('Should fail if hosts is not an array', async function () {
+      it('Should fail if hosts is not composed by hosts', async function () {
         await makePostBodyRequest({
           url: server.url,
           path,
+          fields: { hosts: [ 'localhost:9002', 'localhost:coucou' ] },
           token: server.accessToken,
-          fields: { hosts: 'localhost:9002' },
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       })
 
-      it('Should fail if the array is not composed by hosts', async function () {
+      it('Should fail if hosts is composed with http schemes', async function () {
         await makePostBodyRequest({
           url: server.url,
           path,
-          fields: { hosts: [ 'localhost:9002', 'localhost:coucou' ] },
+          fields: { hosts: [ 'localhost:9002', 'http://localhost:9003' ] },
+          token: server.accessToken,
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
+        })
+      })
+
+      it('Should fail if hosts are not unique', async function () {
+        await makePostBodyRequest({
+          url: server.url,
+          path,
+          fields: { urls: [ 'localhost:9002', 'localhost:9002' ] },
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       })
 
-      it('Should fail if the array is composed with http schemes', async function () {
+      it('Should fail if handles is not composed by handles', async function () {
         await makePostBodyRequest({
           url: server.url,
           path,
-          fields: { hosts: [ 'localhost:9002', 'http://localhost:9003' ] },
+          fields: { handles: [ 'hello@example.com', 'localhost:9001' ] },
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       })
 
-      it('Should fail if hosts are not unique', async function () {
+      it('Should fail if handles are not unique', async function () {
         await makePostBodyRequest({
           url: server.url,
           path,
-          fields: { urls: [ 'localhost:9002', 'localhost:9002' ] },
+          fields: { urls: [ 'hello@example.com', 'hello@example.com' ] },
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       })
 
@@ -103,7 +103,7 @@ describe('Test server follows API validators', function () {
           path,
           fields: { hosts: [ 'localhost:9002' ] },
           token: 'fake_token',
-          statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401
         })
       })
 
@@ -113,7 +113,7 @@ describe('Test server follows API validators', function () {
           path,
           fields: { hosts: [ 'localhost:9002' ] },
           token: userAccessToken,
-          statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+          expectedStatus: HttpStatusCode.FORBIDDEN_403
         })
       })
     })
@@ -157,7 +157,7 @@ describe('Test server follows API validators', function () {
         await makeGetRequest({
           url: server.url,
           path,
-          statusCodeExpected: HttpStatusCode.OK_200,
+          expectedStatus: HttpStatusCode.OK_200,
           query: {
             state: 'accepted',
             actorType: 'Application'
@@ -206,7 +206,7 @@ describe('Test server follows API validators', function () {
         await makeGetRequest({
           url: server.url,
           path,
-          statusCodeExpected: HttpStatusCode.OK_200,
+          expectedStatus: HttpStatusCode.OK_200,
           query: {
             state: 'accepted'
           }
@@ -222,7 +222,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto@localhost:9002',
           token: 'fake_token',
-          statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401
         })
       })
 
@@ -231,7 +231,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto@localhost:9002',
           token: userAccessToken,
-          statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+          expectedStatus: HttpStatusCode.FORBIDDEN_403
         })
       })
 
@@ -240,7 +240,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto',
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       })
 
@@ -249,7 +249,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto@localhost:9003',
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+          expectedStatus: HttpStatusCode.NOT_FOUND_404
         })
       })
     })
@@ -262,7 +262,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto@localhost:9002/accept',
           token: 'fake_token',
-          statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401
         })
       })
 
@@ -271,7 +271,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto@localhost:9002/accept',
           token: userAccessToken,
-          statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+          expectedStatus: HttpStatusCode.FORBIDDEN_403
         })
       })
 
@@ -280,7 +280,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto/accept',
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       })
 
@@ -289,7 +289,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto@localhost:9003/accept',
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+          expectedStatus: HttpStatusCode.NOT_FOUND_404
         })
       })
     })
@@ -302,7 +302,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto@localhost:9002/reject',
           token: 'fake_token',
-          statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401
         })
       })
 
@@ -311,7 +311,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto@localhost:9002/reject',
           token: userAccessToken,
-          statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+          expectedStatus: HttpStatusCode.FORBIDDEN_403
         })
       })
 
@@ -320,7 +320,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto/reject',
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       })
 
@@ -329,7 +329,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/toto@localhost:9003/reject',
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+          expectedStatus: HttpStatusCode.NOT_FOUND_404
         })
       })
     })
@@ -342,7 +342,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/localhost:9002',
           token: 'fake_token',
-          statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401
         })
       })
 
@@ -351,7 +351,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/localhost:9002',
           token: userAccessToken,
-          statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+          expectedStatus: HttpStatusCode.FORBIDDEN_403
         })
       })
 
@@ -360,7 +360,7 @@ describe('Test server follows API validators', function () {
           url: server.url,
           path: path + '/example.com',
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+          expectedStatus: HttpStatusCode.NOT_FOUND_404
         })
       })
     })
index 3c1d2049bbbcb7e00dd101a286e1956f20e94be9..23d95d8e4c502b10abf11d6f3407827cac2689b4 100644 (file)
@@ -1,26 +1,21 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
-import {
-  cleanupTests,
-  createUser,
-  flushAndRunServer,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
 import {
   checkBadCountPagination,
   checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { makeGetRequest } from '../../../../shared/extra-utils/requests/requests'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+  checkBadStartPagination,
+  cleanupTests,
+  createSingleServer,
+  makeGetRequest,
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test jobs API validators', function () {
   const path = '/api/v1/jobs/failed'
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken = ''
 
   // ---------------------------------------------------------------
@@ -28,7 +23,7 @@ describe('Test jobs API validators', function () {
   before(async function () {
     this.timeout(120000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
@@ -36,8 +31,8 @@ describe('Test jobs API validators', function () {
       username: 'user1',
       password: 'my super password'
     }
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-    userAccessToken = await userLogin(server, user)
+    await server.users.create({ username: user.username, password: user.password })
+    userAccessToken = await server.login.getAccessToken(user)
   })
 
   describe('When listing jobs', function () {
@@ -77,7 +72,7 @@ describe('Test jobs API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -86,7 +81,7 @@ describe('Test jobs API validators', function () {
         url: server.url,
         path,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
index 933d8abf2c615c5b889d3ace38582d78a50c6018..700b4724dff29cf8807b87a99e8824bce47a6ddf 100644 (file)
@@ -2,69 +2,64 @@
 
 import 'mocha'
 import { omit } from 'lodash'
-import { LiveVideo, VideoCreateResult, VideoPrivacy } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   buildAbsoluteFixturePath,
   cleanupTests,
-  createUser,
-  flushAndRunServer,
-  getLive,
-  getMyUserInformation,
-  immutableAssign,
+  createSingleServer,
+  LiveCommand,
   makePostBodyRequest,
   makeUploadRequest,
-  runAndTestFfmpegStreamError,
+  PeerTubeServer,
   sendRTMPStream,
-  ServerInfo,
   setAccessTokensToServers,
-  stopFfmpeg,
-  updateCustomSubConfig,
-  updateLive,
-  uploadVideoAndGetId,
-  userLogin,
-  waitUntilLivePublished
-} from '../../../../shared/extra-utils'
+  stopFfmpeg
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
 
 describe('Test video lives API validator', function () {
   const path = '/api/v1/videos/live'
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken = ''
   let channelId: number
   let video: VideoCreateResult
   let videoIdNotLive: number
+  let command: LiveCommand
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
-    await updateCustomSubConfig(server.url, server.accessToken, {
-      live: {
-        enabled: true,
-        maxInstanceLives: 20,
-        maxUserLives: 20,
-        allowReplay: true
+    await server.config.updateCustomSubConfig({
+      newConfig: {
+        live: {
+          enabled: true,
+          maxInstanceLives: 20,
+          maxUserLives: 20,
+          allowReplay: true
+        }
       }
     })
 
     const username = 'user1'
     const password = 'my super password'
-    await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
-    userAccessToken = await userLogin(server, { username, password })
+    await server.users.create({ username: username, password: password })
+    userAccessToken = await server.login.getAccessToken({ username, password })
 
     {
-      const res = await getMyUserInformation(server.url, server.accessToken)
-      channelId = res.body.videoChannels[0].id
+      const { videoChannels } = await server.users.getMyInfo()
+      channelId = videoChannels[0].id
     }
 
     {
-      videoIdNotLive = (await uploadVideoAndGetId({ server, videoName: 'not live' })).id
+      videoIdNotLive = (await server.videos.quickUpload({ name: 'not live' })).id
     }
+
+    command = server.live
   })
 
   describe('When creating a live', function () {
@@ -96,37 +91,37 @@ describe('Test video lives API validator', function () {
     })
 
     it('Should fail with a long name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) })
+      const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad category', async function () {
-      const fields = immutableAssign(baseCorrectParams, { category: 125 })
+      const fields = { ...baseCorrectParams, category: 125 }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad licence', async function () {
-      const fields = immutableAssign(baseCorrectParams, { licence: 125 })
+      const fields = { ...baseCorrectParams, licence: 125 }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad language', async function () {
-      const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) })
+      const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a long description', async function () {
-      const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
+      const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a long support text', async function () {
-      const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
+      const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -138,7 +133,7 @@ describe('Test video lives API validator', function () {
     })
 
     it('Should fail with a bad channel', async function () {
-      const fields = immutableAssign(baseCorrectParams, { channelId: 545454 })
+      const fields = { ...baseCorrectParams, channelId: 545454 }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -148,31 +143,31 @@ describe('Test video lives API validator', function () {
         username: 'fake',
         password: 'fake_password'
       }
-      await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
+      await server.users.create({ username: user.username, password: user.password })
 
-      const accessTokenUser = await userLogin(server, user)
-      const res = await getMyUserInformation(server.url, accessTokenUser)
-      const customChannelId = res.body.videoChannels[0].id
+      const accessTokenUser = await server.login.getAccessToken(user)
+      const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser })
+      const customChannelId = videoChannels[0].id
 
-      const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId })
+      const fields = { ...baseCorrectParams, channelId: customChannelId }
 
       await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
     })
 
     it('Should fail with too many tags', async function () {
-      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] })
+      const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a tag length too low', async function () {
-      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] })
+      const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a tag length too big', async function () {
-      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] })
+      const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -214,7 +209,7 @@ describe('Test video lives API validator', function () {
     })
 
     it('Should fail with save replay and permanent live set to true', async function () {
-      const fields = immutableAssign(baseCorrectParams, { saveReplay: true, permanentLive: true })
+      const fields = { ...baseCorrectParams, saveReplay: true, permanentLive: true }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -227,16 +222,18 @@ describe('Test video lives API validator', function () {
         path,
         token: server.accessToken,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       video = res.body.video
     })
 
     it('Should forbid if live is disabled', async function () {
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        live: {
-          enabled: false
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          live: {
+            enabled: false
+          }
         }
       })
 
@@ -245,17 +242,19 @@ describe('Test video lives API validator', function () {
         path,
         token: server.accessToken,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
     it('Should forbid to save replay if not enabled by the admin', async function () {
-      const fields = immutableAssign(baseCorrectParams, { saveReplay: true })
-
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        live: {
-          enabled: true,
-          allowReplay: false
+      const fields = { ...baseCorrectParams, saveReplay: true }
+
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          live: {
+            enabled: true,
+            allowReplay: false
+          }
         }
       })
 
@@ -264,17 +263,19 @@ describe('Test video lives API validator', function () {
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
     it('Should allow to save replay if enabled by the admin', async function () {
-      const fields = immutableAssign(baseCorrectParams, { saveReplay: true })
-
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        live: {
-          enabled: true,
-          allowReplay: true
+      const fields = { ...baseCorrectParams, saveReplay: true }
+
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          live: {
+            enabled: true,
+            allowReplay: true
+          }
         }
       })
 
@@ -283,15 +284,17 @@ describe('Test video lives API validator', function () {
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
 
     it('Should not allow live if max instance lives is reached', async function () {
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        live: {
-          enabled: true,
-          maxInstanceLives: 1
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          live: {
+            enabled: true,
+            maxInstanceLives: 1
+          }
         }
       })
 
@@ -300,16 +303,18 @@ describe('Test video lives API validator', function () {
         path,
         token: server.accessToken,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
     it('Should not allow live if max user lives is reached', async function () {
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        live: {
-          enabled: true,
-          maxInstanceLives: 20,
-          maxUserLives: 1
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          live: {
+            enabled: true,
+            maxInstanceLives: 20,
+            maxUserLives: 1
+          }
         }
       })
 
@@ -318,7 +323,7 @@ describe('Test video lives API validator', function () {
         path,
         token: server.accessToken,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
   })
@@ -326,110 +331,112 @@ describe('Test video lives API validator', function () {
   describe('When getting live information', function () {
 
     it('Should fail without access token', async function () {
-      await getLive(server.url, '', video.id, HttpStatusCode.UNAUTHORIZED_401)
+      await command.get({ token: '', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a bad access token', async function () {
-      await getLive(server.url, 'toto', video.id, HttpStatusCode.UNAUTHORIZED_401)
+      await command.get({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with access token of another user', async function () {
-      await getLive(server.url, userAccessToken, video.id, HttpStatusCode.FORBIDDEN_403)
+      await command.get({ token: userAccessToken, videoId: video.id, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with a bad video id', async function () {
-      await getLive(server.url, server.accessToken, 'toto', HttpStatusCode.BAD_REQUEST_400)
+      await command.get({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should fail with an unknown video id', async function () {
-      await getLive(server.url, server.accessToken, 454555, HttpStatusCode.NOT_FOUND_404)
+      await command.get({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with a non live video', async function () {
-      await getLive(server.url, server.accessToken, videoIdNotLive, HttpStatusCode.NOT_FOUND_404)
+      await command.get({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should succeed with the correct params', async function () {
-      await getLive(server.url, server.accessToken, video.id)
-      await getLive(server.url, server.accessToken, video.shortUUID)
+      await command.get({ videoId: video.id })
+      await command.get({ videoId: video.uuid })
+      await command.get({ videoId: video.shortUUID })
     })
   })
 
   describe('When updating live information', async function () {
 
     it('Should fail without access token', async function () {
-      await updateLive(server.url, '', video.id, {}, HttpStatusCode.UNAUTHORIZED_401)
+      await command.update({ token: '', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a bad access token', async function () {
-      await updateLive(server.url, 'toto', video.id, {}, HttpStatusCode.UNAUTHORIZED_401)
+      await command.update({ token: 'toto', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with access token of another user', async function () {
-      await updateLive(server.url, userAccessToken, video.id, {}, HttpStatusCode.FORBIDDEN_403)
+      await command.update({ token: userAccessToken, videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with a bad video id', async function () {
-      await updateLive(server.url, server.accessToken, 'toto', {}, HttpStatusCode.BAD_REQUEST_400)
+      await command.update({ videoId: 'toto', fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should fail with an unknown video id', async function () {
-      await updateLive(server.url, server.accessToken, 454555, {}, HttpStatusCode.NOT_FOUND_404)
+      await command.update({ videoId: 454555, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with a non live video', async function () {
-      await updateLive(server.url, server.accessToken, videoIdNotLive, {}, HttpStatusCode.NOT_FOUND_404)
+      await command.update({ videoId: videoIdNotLive, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with save replay and permanent live set to true', async function () {
       const fields = { saveReplay: true, permanentLive: true }
 
-      await updateLive(server.url, server.accessToken, video.id, fields, HttpStatusCode.BAD_REQUEST_400)
+      await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should succeed with the correct params', async function () {
-      await updateLive(server.url, server.accessToken, video.id, { saveReplay: false })
-      await updateLive(server.url, server.accessToken, video.shortUUID, { saveReplay: false })
+      await command.update({ videoId: video.id, fields: { saveReplay: false } })
+      await command.update({ videoId: video.uuid, fields: { saveReplay: false } })
+      await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } })
     })
 
     it('Should fail to update replay status if replay is not allowed on the instance', async function () {
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        live: {
-          enabled: true,
-          allowReplay: false
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          live: {
+            enabled: true,
+            allowReplay: false
+          }
         }
       })
 
-      await updateLive(server.url, server.accessToken, video.id, { saveReplay: true }, HttpStatusCode.FORBIDDEN_403)
+      await command.update({ videoId: video.id, fields: { saveReplay: true }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail to update a live if it has already started', async function () {
       this.timeout(40000)
 
-      const resLive = await getLive(server.url, server.accessToken, video.id)
-      const live: LiveVideo = resLive.body
+      const live = await command.get({ videoId: video.id })
 
-      const command = sendRTMPStream(live.rtmpUrl, live.streamKey)
+      const ffmpegCommand = sendRTMPStream(live.rtmpUrl, live.streamKey)
 
-      await waitUntilLivePublished(server.url, server.accessToken, video.id)
-      await updateLive(server.url, server.accessToken, video.id, {}, HttpStatusCode.BAD_REQUEST_400)
+      await command.waitUntilPublished({ videoId: video.id })
+      await command.update({ videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
 
-      await stopFfmpeg(command)
+      await stopFfmpeg(ffmpegCommand)
     })
 
     it('Should fail to stream twice in the save live', async function () {
       this.timeout(40000)
 
-      const resLive = await getLive(server.url, server.accessToken, video.id)
-      const live: LiveVideo = resLive.body
+      const live = await command.get({ videoId: video.id })
 
-      const command = sendRTMPStream(live.rtmpUrl, live.streamKey)
+      const ffmpegCommand = sendRTMPStream(live.rtmpUrl, live.streamKey)
 
-      await waitUntilLivePublished(server.url, server.accessToken, video.id)
+      await command.waitUntilPublished({ videoId: video.id })
 
-      await runAndTestFfmpegStreamError(server.url, server.accessToken, video.id, true)
+      await command.runAndTestStreamError({ videoId: video.id, shouldHaveError: true })
 
-      await stopFfmpeg(command)
+      await stopFfmpeg(ffmpegCommand)
     })
   })
 
index dac1e6b98d2cd777ec5314436bc733b2e9f1680e..05372257a7addca2c1d93c43cf803175cea097aa 100644 (file)
@@ -1,21 +1,12 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
-import {
-  cleanupTests,
-  createUser,
-  flushAndRunServer,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { makeGetRequest } from '../../../../shared/extra-utils/requests/requests'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { cleanupTests, createSingleServer, makeGetRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test logs API validators', function () {
   const path = '/api/v1/server/logs'
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken = ''
 
   // ---------------------------------------------------------------
@@ -23,7 +14,7 @@ describe('Test logs API validators', function () {
   before(async function () {
     this.timeout(120000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
@@ -31,8 +22,8 @@ describe('Test logs API validators', function () {
       username: 'user1',
       password: 'my super password'
     }
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-    userAccessToken = await userLogin(server, user)
+    await server.users.create({ username: user.username, password: user.password })
+    userAccessToken = await server.login.getAccessToken(user)
   })
 
   describe('When getting logs', function () {
@@ -41,7 +32,7 @@ describe('Test logs API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -50,7 +41,7 @@ describe('Test logs API validators', function () {
         url: server.url,
         path,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -59,7 +50,7 @@ describe('Test logs API validators', function () {
         url: server.url,
         path,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -69,7 +60,7 @@ describe('Test logs API validators', function () {
         path,
         token: server.accessToken,
         query: { startDate: 'toto' },
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -79,7 +70,7 @@ describe('Test logs API validators', function () {
         path,
         token: server.accessToken,
         query: { startDate: new Date().toISOString(), endDate: 'toto' },
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -89,7 +80,7 @@ describe('Test logs API validators', function () {
         path,
         token: server.accessToken,
         query: { startDate: new Date().toISOString(), level: 'toto' },
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -99,7 +90,7 @@ describe('Test logs API validators', function () {
         path,
         token: server.accessToken,
         query: { startDate: new Date().toISOString() },
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
index a833fe6ffe03fdccb4b2c3ee828acf47603ddd24..33f84ecbc4d0d2c8b7141c27c9d2bb73f8f59872 100644 (file)
@@ -1,27 +1,22 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { HttpStatusCode } from '@shared/core-utils'
 import {
   checkBadCountPagination,
   checkBadSortPagination,
   checkBadStartPagination,
   cleanupTests,
-  createUser,
-  flushAndRunServer,
-  immutableAssign,
-  installPlugin,
+  createSingleServer,
   makeGetRequest,
   makePostBodyRequest,
   makePutBodyRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
+  PeerTubeServer,
+  setAccessTokensToServers
 } from '@shared/extra-utils'
-import { PeerTubePlugin, PluginType } from '@shared/models'
+import { HttpStatusCode, PeerTubePlugin, PluginType } from '@shared/models'
 
 describe('Test server plugins API validators', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken = null
 
   const npmPlugin = 'peertube-plugin-hello-world'
@@ -37,7 +32,7 @@ describe('Test server plugins API validators', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
@@ -46,17 +41,17 @@ describe('Test server plugins API validators', function () {
       password: 'password'
     }
 
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-    userAccessToken = await userLogin(server, user)
+    await server.users.create({ username: user.username, password: user.password })
+    userAccessToken = await server.login.getAccessToken(user)
 
     {
-      const res = await installPlugin({ url: server.url, accessToken: server.accessToken, npmName: npmPlugin })
+      const res = await server.plugins.install({ npmName: npmPlugin })
       const plugin = res.body as PeerTubePlugin
       npmVersion = plugin.version
     }
 
     {
-      const res = await installPlugin({ url: server.url, accessToken: server.accessToken, npmName: themePlugin })
+      const res = await server.plugins.install({ npmName: themePlugin })
       const plugin = res.body as PeerTubePlugin
       themeVersion = plugin.version
     }
@@ -74,7 +69,7 @@ describe('Test server plugins API validators', function () {
       ]
 
       for (const p of paths) {
-        await makeGetRequest({ url: server.url, path: p, statusCodeExpected: HttpStatusCode.NOT_FOUND_404 })
+        await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
       }
     })
 
@@ -82,7 +77,7 @@ describe('Test server plugins API validators', function () {
       await makeGetRequest({
         url: server.url,
         path: '/themes/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png',
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -97,7 +92,7 @@ describe('Test server plugins API validators', function () {
       ]
 
       for (const p of paths) {
-        await makeGetRequest({ url: server.url, path: p, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+        await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
       }
     })
 
@@ -111,14 +106,14 @@ describe('Test server plugins API validators', function () {
       ]
 
       for (const p of paths) {
-        await makeGetRequest({ url: server.url, path: p, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+        await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
       }
     })
 
     it('Should fail with an unknown auth name', async function () {
       const path = '/plugins/' + pluginName + '/' + npmVersion + '/auth/bad-auth'
 
-      await makeGetRequest({ url: server.url, path, statusCodeExpected: HttpStatusCode.NOT_FOUND_404 })
+      await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with an unknown static file', async function () {
@@ -130,7 +125,7 @@ describe('Test server plugins API validators', function () {
       ]
 
       for (const p of paths) {
-        await makeGetRequest({ url: server.url, path: p, statusCodeExpected: HttpStatusCode.NOT_FOUND_404 })
+        await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
       }
     })
 
@@ -138,7 +133,7 @@ describe('Test server plugins API validators', function () {
       await makeGetRequest({
         url: server.url,
         path: '/themes/' + themeName + '/' + themeVersion + '/css/assets/fake.css',
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -152,11 +147,11 @@ describe('Test server plugins API validators', function () {
       ]
 
       for (const p of paths) {
-        await makeGetRequest({ url: server.url, path: p, statusCodeExpected: HttpStatusCode.OK_200 })
+        await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.OK_200 })
       }
 
       const authPath = '/plugins/' + pluginName + '/' + npmVersion + '/auth/fake-auth'
-      await makeGetRequest({ url: server.url, path: authPath, statusCodeExpected: HttpStatusCode.FOUND_302 })
+      await makeGetRequest({ url: server.url, path: authPath, expectedStatus: HttpStatusCode.FOUND_302 })
     })
   })
 
@@ -174,7 +169,7 @@ describe('Test server plugins API validators', function () {
         path,
         token: 'fake_token',
         query: baseQuery,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -184,7 +179,7 @@ describe('Test server plugins API validators', function () {
         path,
         token: userAccessToken,
         query: baseQuery,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -201,7 +196,7 @@ describe('Test server plugins API validators', function () {
     })
 
     it('Should fail with an invalid plugin type', async function () {
-      const query = immutableAssign(baseQuery, { pluginType: 5 })
+      const query = { ...baseQuery, pluginType: 5 }
 
       await makeGetRequest({
         url: server.url,
@@ -212,7 +207,7 @@ describe('Test server plugins API validators', function () {
     })
 
     it('Should fail with an invalid current peertube engine', async function () {
-      const query = immutableAssign(baseQuery, { currentPeerTubeEngine: '1.0' })
+      const query = { ...baseQuery, currentPeerTubeEngine: '1.0' }
 
       await makeGetRequest({
         url: server.url,
@@ -228,7 +223,7 @@ describe('Test server plugins API validators', function () {
         path,
         token: server.accessToken,
         query: baseQuery,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -245,7 +240,7 @@ describe('Test server plugins API validators', function () {
         path,
         token: 'fake_token',
         query: baseQuery,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -255,7 +250,7 @@ describe('Test server plugins API validators', function () {
         path,
         token: userAccessToken,
         query: baseQuery,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -272,7 +267,7 @@ describe('Test server plugins API validators', function () {
     })
 
     it('Should fail with an invalid plugin type', async function () {
-      const query = immutableAssign(baseQuery, { pluginType: 5 })
+      const query = { ...baseQuery, pluginType: 5 }
 
       await makeGetRequest({
         url: server.url,
@@ -288,7 +283,7 @@ describe('Test server plugins API validators', function () {
         path,
         token: server.accessToken,
         query: baseQuery,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -302,7 +297,7 @@ describe('Test server plugins API validators', function () {
           url: server.url,
           path: path + suffix,
           token: 'fake_token',
-          statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401
         })
       }
     })
@@ -313,7 +308,7 @@ describe('Test server plugins API validators', function () {
           url: server.url,
           path: path + suffix,
           token: userAccessToken,
-          statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+          expectedStatus: HttpStatusCode.FORBIDDEN_403
         })
       }
     })
@@ -324,7 +319,7 @@ describe('Test server plugins API validators', function () {
           url: server.url,
           path: path + suffix,
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       }
 
@@ -333,7 +328,7 @@ describe('Test server plugins API validators', function () {
           url: server.url,
           path: path + suffix,
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       }
     })
@@ -344,7 +339,7 @@ describe('Test server plugins API validators', function () {
           url: server.url,
           path: path + suffix,
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+          expectedStatus: HttpStatusCode.NOT_FOUND_404
         })
       }
     })
@@ -355,7 +350,7 @@ describe('Test server plugins API validators', function () {
           url: server.url,
           path: path + suffix,
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.OK_200
+          expectedStatus: HttpStatusCode.OK_200
         })
       }
     })
@@ -371,7 +366,7 @@ describe('Test server plugins API validators', function () {
         path: path + npmPlugin + '/settings',
         fields: { settings },
         token: 'fake_token',
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -381,7 +376,7 @@ describe('Test server plugins API validators', function () {
         path: path + npmPlugin + '/settings',
         fields: { settings },
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -391,7 +386,7 @@ describe('Test server plugins API validators', function () {
         path: path + 'toto/settings',
         fields: { settings },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
 
       await makePutBodyRequest({
@@ -399,7 +394,7 @@ describe('Test server plugins API validators', function () {
         path: path + 'peertube-plugin-TOTO/settings',
         fields: { settings },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -409,7 +404,7 @@ describe('Test server plugins API validators', function () {
         path: path + 'peertube-plugin-toto/settings',
         fields: { settings },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -419,7 +414,7 @@ describe('Test server plugins API validators', function () {
         path: path + npmPlugin + '/settings',
         fields: { settings },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -434,7 +429,7 @@ describe('Test server plugins API validators', function () {
           path: path + suffix,
           fields: { npmName: npmPlugin },
           token: 'fake_token',
-          statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401
         })
       }
     })
@@ -446,7 +441,7 @@ describe('Test server plugins API validators', function () {
           path: path + suffix,
           fields: { npmName: npmPlugin },
           token: userAccessToken,
-          statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+          expectedStatus: HttpStatusCode.FORBIDDEN_403
         })
       }
     })
@@ -458,7 +453,7 @@ describe('Test server plugins API validators', function () {
           path: path + suffix,
           fields: { npmName: 'toto' },
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       }
 
@@ -468,7 +463,7 @@ describe('Test server plugins API validators', function () {
           path: path + suffix,
           fields: { npmName: 'peertube-plugin-TOTO' },
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       }
     })
@@ -488,7 +483,7 @@ describe('Test server plugins API validators', function () {
           path: path + obj.suffix,
           fields: { npmName: npmPlugin },
           token: server.accessToken,
-          statusCodeExpected: obj.status
+          expectedStatus: obj.status
         })
       }
     })
index dac6938de1296c90227ed995c9637c995ccd22f0..d9f9055496b7260fbf0d7135be1927fe7422e890 100644 (file)
@@ -1,30 +1,25 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { VideoCreateResult } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   checkBadCountPagination,
   checkBadSortPagination,
   checkBadStartPagination,
   cleanupTests,
-  createUser,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getVideo,
   makeDeleteRequest,
   makeGetRequest,
   makePostBodyRequest,
   makePutBodyRequest,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  uploadVideoAndGetId,
-  userLogin,
   waitJobs
-} from '../../../../shared/extra-utils'
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoCreateResult } from '@shared/models'
 
 describe('Test server redundancy API validators', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let userAccessToken = null
   let videoIdLocal: number
   let videoRemote: VideoCreateResult
@@ -34,7 +29,7 @@ describe('Test server redundancy API validators', function () {
   before(async function () {
     this.timeout(80000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
     await doubleFollow(servers[0], servers[1])
@@ -44,17 +39,16 @@ describe('Test server redundancy API validators', function () {
       password: 'password'
     }
 
-    await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
-    userAccessToken = await userLogin(servers[0], user)
+    await servers[0].users.create({ username: user.username, password: user.password })
+    userAccessToken = await servers[0].login.getAccessToken(user)
 
-    videoIdLocal = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video' })).id
+    videoIdLocal = (await servers[0].videos.quickUpload({ name: 'video' })).id
 
-    const remoteUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video' })).uuid
+    const remoteUUID = (await servers[1].videos.quickUpload({ name: 'video' })).uuid
 
     await waitJobs(servers)
 
-    const resVideo = await getVideo(servers[0].url, remoteUUID)
-    videoRemote = resVideo.body
+    videoRemote = await servers[0].videos.get({ id: remoteUUID })
   })
 
   describe('When listing redundancies', function () {
@@ -69,11 +63,11 @@ describe('Test server redundancy API validators', function () {
     })
 
     it('Should fail with an invalid token', async function () {
-      await makeGetRequest({ url, path, token: 'fake_token', statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makeGetRequest({ url, path, token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail if the user is not an administrator', async function () {
-      await makeGetRequest({ url, path, token: userAccessToken, statusCodeExpected: HttpStatusCode.FORBIDDEN_403 })
+      await makeGetRequest({ url, path, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with a bad start pagination', async function () {
@@ -97,7 +91,7 @@ describe('Test server redundancy API validators', function () {
     })
 
     it('Should succeed with the correct params', async function () {
-      await makeGetRequest({ url, path, token, query: { target: 'my-videos' }, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url, path, token, query: { target: 'my-videos' }, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -113,11 +107,11 @@ describe('Test server redundancy API validators', function () {
     })
 
     it('Should fail with an invalid token', async function () {
-      await makePostBodyRequest({ url, path, token: 'fake_token', statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makePostBodyRequest({ url, path, token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail if the user is not an administrator', async function () {
-      await makePostBodyRequest({ url, path, token: userAccessToken, statusCodeExpected: HttpStatusCode.FORBIDDEN_403 })
+      await makePostBodyRequest({ url, path, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail without a video id', async function () {
@@ -129,7 +123,7 @@ describe('Test server redundancy API validators', function () {
     })
 
     it('Should fail with a not found video id', async function () {
-      await makePostBodyRequest({ url, path, token, fields: { videoId: 6565 }, statusCodeExpected: HttpStatusCode.NOT_FOUND_404 })
+      await makePostBodyRequest({ url, path, token, fields: { videoId: 6565 }, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with a local a video id', async function () {
@@ -142,7 +136,7 @@ describe('Test server redundancy API validators', function () {
         path,
         token,
         fields: { videoId: videoRemote.shortUUID },
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
 
@@ -156,7 +150,7 @@ describe('Test server redundancy API validators', function () {
         path,
         token,
         fields: { videoId: videoRemote.uuid },
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
   })
@@ -173,11 +167,11 @@ describe('Test server redundancy API validators', function () {
     })
 
     it('Should fail with an invalid token', async function () {
-      await makeDeleteRequest({ url, path: path + '1', token: 'fake_token', statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makeDeleteRequest({ url, path: path + '1', token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail if the user is not an administrator', async function () {
-      await makeDeleteRequest({ url, path: path + '1', token: userAccessToken, statusCodeExpected: HttpStatusCode.FORBIDDEN_403 })
+      await makeDeleteRequest({ url, path: path + '1', token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with an incorrect video id', async function () {
@@ -185,7 +179,7 @@ describe('Test server redundancy API validators', function () {
     })
 
     it('Should fail with a not found video redundancy', async function () {
-      await makeDeleteRequest({ url, path: path + '454545', token, statusCodeExpected: HttpStatusCode.NOT_FOUND_404 })
+      await makeDeleteRequest({ url, path: path + '454545', token, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
   })
 
@@ -198,7 +192,7 @@ describe('Test server redundancy API validators', function () {
         path: path + '/localhost:' + servers[1].port,
         fields: { redundancyAllowed: true },
         token: 'fake_token',
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -208,7 +202,7 @@ describe('Test server redundancy API validators', function () {
         path: path + '/localhost:' + servers[1].port,
         fields: { redundancyAllowed: true },
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -218,7 +212,7 @@ describe('Test server redundancy API validators', function () {
         path: path + '/example.com',
         fields: { redundancyAllowed: true },
         token: servers[0].accessToken,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -228,7 +222,7 @@ describe('Test server redundancy API validators', function () {
         path: path + '/localhost:' + servers[1].port,
         fields: { blabla: true },
         token: servers[0].accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -238,7 +232,7 @@ describe('Test server redundancy API validators', function () {
         path: path + '/localhost:' + servers[1].port,
         fields: { redundancyAllowed: true },
         token: servers[0].accessToken,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
index 20ad46cff1228d9ccd72a2b58174b40645824b20..cc15d25930865bf8a3a765266a2cbed508c7068d 100644 (file)
@@ -2,41 +2,39 @@
 
 import 'mocha'
 import {
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   cleanupTests,
-  flushAndRunServer,
-  immutableAssign,
+  createSingleServer,
   makeGetRequest,
-  ServerInfo,
-  updateCustomSubConfig,
+  PeerTubeServer,
   setAccessTokensToServers
-} from '../../../../shared/extra-utils'
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-
-function updateSearchIndex (server: ServerInfo, enabled: boolean, disableLocalSearch = false) {
-  return updateCustomSubConfig(server.url, server.accessToken, {
-    search: {
-      searchIndex: {
-        enabled,
-        disableLocalSearch
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
+
+function updateSearchIndex (server: PeerTubeServer, enabled: boolean, disableLocalSearch = false) {
+  return server.config.updateCustomSubConfig({
+    newConfig: {
+      search: {
+        searchIndex: {
+          enabled,
+          disableLocalSearch
+        }
       }
     }
   })
 }
 
 describe('Test videos API validator', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
   })
 
@@ -59,84 +57,104 @@ describe('Test videos API validator', function () {
       await checkBadSortPagination(server.url, path, null, query)
     })
 
-    it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path, query, statusCodeExpected: HttpStatusCode.OK_200 })
+    it('Should succeed with the correct parameters', async function () {
+      await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
     })
 
     it('Should fail with an invalid category', async function () {
-      const customQuery1 = immutableAssign(query, { categoryOneOf: [ 'aa', 'b' ] })
-      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+      const customQuery1 = { ...query, categoryOneOf: [ 'aa', 'b' ] }
+      await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
 
-      const customQuery2 = immutableAssign(query, { categoryOneOf: 'a' })
-      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+      const customQuery2 = { ...query, categoryOneOf: 'a' }
+      await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should succeed with a valid category', async function () {
-      const customQuery1 = immutableAssign(query, { categoryOneOf: [ 1, 7 ] })
-      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: HttpStatusCode.OK_200 })
+      const customQuery1 = { ...query, categoryOneOf: [ 1, 7 ] }
+      await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
 
-      const customQuery2 = immutableAssign(query, { categoryOneOf: 1 })
-      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: HttpStatusCode.OK_200 })
+      const customQuery2 = { ...query, categoryOneOf: 1 }
+      await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
     })
 
     it('Should fail with an invalid licence', async function () {
-      const customQuery1 = immutableAssign(query, { licenceOneOf: [ 'aa', 'b' ] })
-      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+      const customQuery1 = { ...query, licenceOneOf: [ 'aa', 'b' ] }
+      await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
 
-      const customQuery2 = immutableAssign(query, { licenceOneOf: 'a' })
-      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+      const customQuery2 = { ...query, licenceOneOf: 'a' }
+      await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should succeed with a valid licence', async function () {
-      const customQuery1 = immutableAssign(query, { licenceOneOf: [ 1, 2 ] })
-      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: HttpStatusCode.OK_200 })
+      const customQuery1 = { ...query, licenceOneOf: [ 1, 2 ] }
+      await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
 
-      const customQuery2 = immutableAssign(query, { licenceOneOf: 1 })
-      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: HttpStatusCode.OK_200 })
+      const customQuery2 = { ...query, licenceOneOf: 1 }
+      await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
     })
 
     it('Should succeed with a valid language', async function () {
-      const customQuery1 = immutableAssign(query, { languageOneOf: [ 'fr', 'en' ] })
-      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: HttpStatusCode.OK_200 })
+      const customQuery1 = { ...query, languageOneOf: [ 'fr', 'en' ] }
+      await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
 
-      const customQuery2 = immutableAssign(query, { languageOneOf: 'fr' })
-      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: HttpStatusCode.OK_200 })
+      const customQuery2 = { ...query, languageOneOf: 'fr' }
+      await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
     })
 
     it('Should succeed with valid tags', async function () {
-      const customQuery1 = immutableAssign(query, { tagsOneOf: [ 'tag1', 'tag2' ] })
-      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: HttpStatusCode.OK_200 })
+      const customQuery1 = { ...query, tagsOneOf: [ 'tag1', 'tag2' ] }
+      await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
 
-      const customQuery2 = immutableAssign(query, { tagsOneOf: 'tag1' })
-      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: HttpStatusCode.OK_200 })
+      const customQuery2 = { ...query, tagsOneOf: 'tag1' }
+      await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
 
-      const customQuery3 = immutableAssign(query, { tagsAllOf: [ 'tag1', 'tag2' ] })
-      await makeGetRequest({ url: server.url, path, query: customQuery3, statusCodeExpected: HttpStatusCode.OK_200 })
+      const customQuery3 = { ...query, tagsAllOf: [ 'tag1', 'tag2' ] }
+      await makeGetRequest({ url: server.url, path, query: customQuery3, expectedStatus: HttpStatusCode.OK_200 })
 
-      const customQuery4 = immutableAssign(query, { tagsAllOf: 'tag1' })
-      await makeGetRequest({ url: server.url, path, query: customQuery4, statusCodeExpected: HttpStatusCode.OK_200 })
+      const customQuery4 = { ...query, tagsAllOf: 'tag1' }
+      await makeGetRequest({ url: server.url, path, query: customQuery4, expectedStatus: HttpStatusCode.OK_200 })
     })
 
     it('Should fail with invalid durations', async function () {
-      const customQuery1 = immutableAssign(query, { durationMin: 'hello' })
-      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+      const customQuery1 = { ...query, durationMin: 'hello' }
+      await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
 
-      const customQuery2 = immutableAssign(query, { durationMax: 'hello' })
-      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+      const customQuery2 = { ...query, durationMax: 'hello' }
+      await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should fail with invalid dates', async function () {
-      const customQuery1 = immutableAssign(query, { startDate: 'hello' })
-      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+      const customQuery1 = { ...query, startDate: 'hello' }
+      await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+
+      const customQuery2 = { ...query, endDate: 'hello' }
+      await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+
+      const customQuery3 = { ...query, originallyPublishedStartDate: 'hello' }
+      await makeGetRequest({ url: server.url, path, query: customQuery3, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+
+      const customQuery4 = { ...query, originallyPublishedEndDate: 'hello' }
+      await makeGetRequest({ url: server.url, path, query: customQuery4, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    })
+
+    it('Should fail with an invalid host', async function () {
+      const customQuery = { ...query, host: '6565' }
+      await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    })
 
-      const customQuery2 = immutableAssign(query, { endDate: 'hello' })
-      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+    it('Should succeed with a host', async function () {
+      const customQuery = { ...query, host: 'example.com' }
+      await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
+    })
 
-      const customQuery3 = immutableAssign(query, { originallyPublishedStartDate: 'hello' })
-      await makeGetRequest({ url: server.url, path, query: customQuery3, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+    it('Should fail with invalid uuids', async function () {
+      const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
+      await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    })
 
-      const customQuery4 = immutableAssign(query, { originallyPublishedEndDate: 'hello' })
-      await makeGetRequest({ url: server.url, path, query: customQuery4, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+    it('Should succeed with valid uuids', async function () {
+      const customQuery = { ...query, uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
+      await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -144,7 +162,8 @@ describe('Test videos API validator', function () {
     const path = '/api/v1/search/video-playlists/'
 
     const query = {
-      search: 'coucou'
+      search: 'coucou',
+      host: 'example.com'
     }
 
     it('Should fail with a bad start pagination', async function () {
@@ -159,8 +178,17 @@ describe('Test videos API validator', function () {
       await checkBadSortPagination(server.url, path, null, query)
     })
 
-    it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path, query, statusCodeExpected: HttpStatusCode.OK_200 })
+    it('Should fail with an invalid host', async function () {
+      await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    })
+
+    it('Should fail with invalid uuids', async function () {
+      const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
+      await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    })
+
+    it('Should succeed with the correct parameters', async function () {
+      await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -168,7 +196,8 @@ describe('Test videos API validator', function () {
     const path = '/api/v1/search/video-channels/'
 
     const query = {
-      search: 'coucou'
+      search: 'coucou',
+      host: 'example.com'
     }
 
     it('Should fail with a bad start pagination', async function () {
@@ -183,8 +212,16 @@ describe('Test videos API validator', function () {
       await checkBadSortPagination(server.url, path, null, query)
     })
 
-    it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path, query, statusCodeExpected: HttpStatusCode.OK_200 })
+    it('Should fail with an invalid host', async function () {
+      await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    })
+
+    it('Should fail with invalid handles', async function () {
+      await makeGetRequest({ url: server.url, path, query: { ...query, handles: [ '' ] }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    })
+
+    it('Should succeed with the correct parameters', async function () {
+      await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -202,42 +239,42 @@ describe('Test videos API validator', function () {
 
       for (const path of paths) {
         {
-          const customQuery = immutableAssign(query, { searchTarget: 'hello' })
-          await makeGetRequest({ url: server.url, path, query: customQuery, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+          const customQuery = { ...query, searchTarget: 'hello' }
+          await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
         }
 
         {
-          const customQuery = immutableAssign(query, { searchTarget: undefined })
-          await makeGetRequest({ url: server.url, path, query: customQuery, statusCodeExpected: HttpStatusCode.OK_200 })
+          const customQuery = { ...query, searchTarget: undefined }
+          await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
         }
 
         {
-          const customQuery = immutableAssign(query, { searchTarget: 'local' })
-          await makeGetRequest({ url: server.url, path, query: customQuery, statusCodeExpected: HttpStatusCode.OK_200 })
+          const customQuery = { ...query, searchTarget: 'local' }
+          await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
         }
 
         {
-          const customQuery = immutableAssign(query, { searchTarget: 'search-index' })
-          await makeGetRequest({ url: server.url, path, query: customQuery, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+          const customQuery = { ...query, searchTarget: 'search-index' }
+          await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
         }
 
         await updateSearchIndex(server, true, true)
 
         {
-          const customQuery = immutableAssign(query, { searchTarget: 'local' })
-          await makeGetRequest({ url: server.url, path, query: customQuery, statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 })
+          const customQuery = { ...query, searchTarget: 'local' }
+          await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
         }
 
         {
-          const customQuery = immutableAssign(query, { searchTarget: 'search-index' })
-          await makeGetRequest({ url: server.url, path, query: customQuery, statusCodeExpected: HttpStatusCode.OK_200 })
+          const customQuery = { ...query, searchTarget: 'search-index' }
+          await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
         }
 
         await updateSearchIndex(server, true, false)
 
         {
-          const customQuery = immutableAssign(query, { searchTarget: 'local' })
-          await makeGetRequest({ url: server.url, path, query: customQuery, statusCodeExpected: HttpStatusCode.OK_200 })
+          const customQuery = { ...query, searchTarget: 'local' }
+          await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
         }
 
         await updateSearchIndex(server, false, false)
index 514e3da702381c890e183432dec8b6b2153ffdcd..8d795fabc232f96763e3fa780c6f736569641e05 100644 (file)
@@ -1,22 +1,18 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
 import {
   cleanupTests,
-  flushAndRunServer,
+  createSingleServer,
   makeGetRequest,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  uploadVideo,
-  createVideoPlaylist,
   setDefaultVideoChannel
-} from '../../../../shared/extra-utils'
-import { VideoPlaylistPrivacy } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models'
 
 describe('Test services API validators', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let playlistUUID: string
 
   // ---------------------------------------------------------------
@@ -24,27 +20,22 @@ describe('Test services API validators', function () {
   before(async function () {
     this.timeout(60000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
     await setDefaultVideoChannel([ server ])
 
-    {
-      const res = await uploadVideo(server.url, server.accessToken, { name: 'my super name' })
-      server.video = res.body.video
-    }
+    server.store.videoCreated = await server.videos.upload({ attributes: { name: 'my super name' } })
 
     {
-      const res = await createVideoPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
+      const created = await server.playlists.create({
+        attributes: {
           displayName: 'super playlist',
           privacy: VideoPlaylistPrivacy.PUBLIC,
-          videoChannelId: server.videoChannel.id
+          videoChannelId: server.store.channel.id
         }
       })
 
-      playlistUUID = res.body.videoPlaylist.uuid
+      playlistUUID = created.uuid
     }
   })
 
@@ -56,7 +47,7 @@ describe('Test services API validators', function () {
     })
 
     it('Should fail with an invalid host', async function () {
-      const embedUrl = 'http://hello.com/videos/watch/' + server.video.uuid
+      const embedUrl = 'http://hello.com/videos/watch/' + server.store.videoCreated.uuid
       await checkParamEmbed(server, embedUrl)
     })
 
@@ -71,37 +62,37 @@ describe('Test services API validators', function () {
     })
 
     it('Should fail with an invalid path', async function () {
-      const embedUrl = `http://localhost:${server.port}/videos/watchs/${server.video.uuid}`
+      const embedUrl = `http://localhost:${server.port}/videos/watchs/${server.store.videoCreated.uuid}`
 
       await checkParamEmbed(server, embedUrl)
     })
 
     it('Should fail with an invalid max height', async function () {
-      const embedUrl = `http://localhost:${server.port}/videos/watch/${server.video.uuid}`
+      const embedUrl = `http://localhost:${server.port}/videos/watch/${server.store.videoCreated.uuid}`
 
       await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { maxheight: 'hello' })
     })
 
     it('Should fail with an invalid max width', async function () {
-      const embedUrl = `http://localhost:${server.port}/videos/watch/${server.video.uuid}`
+      const embedUrl = `http://localhost:${server.port}/videos/watch/${server.store.videoCreated.uuid}`
 
       await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { maxwidth: 'hello' })
     })
 
     it('Should fail with an invalid format', async function () {
-      const embedUrl = `http://localhost:${server.port}/videos/watch/${server.video.uuid}`
+      const embedUrl = `http://localhost:${server.port}/videos/watch/${server.store.videoCreated.uuid}`
 
       await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { format: 'blabla' })
     })
 
     it('Should fail with a non supported format', async function () {
-      const embedUrl = `http://localhost:${server.port}/videos/watch/${server.video.uuid}`
+      const embedUrl = `http://localhost:${server.port}/videos/watch/${server.store.videoCreated.uuid}`
 
       await checkParamEmbed(server, embedUrl, HttpStatusCode.NOT_IMPLEMENTED_501, { format: 'xml' })
     })
 
     it('Should succeed with the correct params with a video', async function () {
-      const embedUrl = `http://localhost:${server.port}/videos/watch/${server.video.uuid}`
+      const embedUrl = `http://localhost:${server.port}/videos/watch/${server.store.videoCreated.uuid}`
       const query = {
         format: 'json',
         maxheight: 400,
@@ -128,13 +119,13 @@ describe('Test services API validators', function () {
   })
 })
 
-function checkParamEmbed (server: ServerInfo, embedUrl: string, statusCodeExpected = HttpStatusCode.BAD_REQUEST_400, query = {}) {
+function checkParamEmbed (server: PeerTubeServer, embedUrl: string, expectedStatus = HttpStatusCode.BAD_REQUEST_400, query = {}) {
   const path = '/services/oembed'
 
   return makeGetRequest({
     url: server.url,
     path,
     query: Object.assign(query, { url: embedUrl }),
-    statusCodeExpected
+    expectedStatus
   })
 }
index d0fbec4155ab0aa9e50b436303ec090b783d6fa2..322e93d0d53348842c5eb87b73599076d1382dab 100644 (file)
@@ -2,46 +2,39 @@
 
 import 'mocha'
 import { expect } from 'chai'
-import { HttpStatusCode, randomInt } from '@shared/core-utils'
-import { getGoodVideoUrl, getMagnetURI, getMyVideoImports, importVideo } from '@shared/extra-utils/videos/video-imports'
-import { MyUser, VideoImport, VideoImportState, VideoPrivacy } from '@shared/models'
+import { randomInt } from '@shared/core-utils'
 import {
   cleanupTests,
-  flushAndRunServer,
-  getMyUserInformation,
-  immutableAssign,
-  registerUser,
-  ServerInfo,
+  createSingleServer,
+  FIXTURE_URLS,
+  PeerTubeServer,
   setAccessTokensToServers,
   setDefaultVideoChannel,
-  updateUser,
-  uploadVideo,
-  userLogin,
+  VideosCommand,
   waitJobs
-} from '../../../../shared/extra-utils'
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoImportState, VideoPrivacy } from '@shared/models'
 
 describe('Test upload quota', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let rootId: number
+  let command: VideosCommand
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
     await setDefaultVideoChannel([ server ])
 
-    const res = await getMyUserInformation(server.url, server.accessToken)
-    rootId = (res.body as MyUser).id
+    const user = await server.users.getMyInfo()
+    rootId = user.id
 
-    await updateUser({
-      url: server.url,
-      userId: rootId,
-      accessToken: server.accessToken,
-      videoQuota: 42
-    })
+    await server.users.update({ userId: rootId, videoQuota: 42 })
+
+    command = server.videos
   })
 
   describe('When having a video quota', function () {
@@ -50,49 +43,48 @@ describe('Test upload quota', function () {
       this.timeout(30000)
 
       const user = { username: 'registered' + randomInt(1, 1500), password: 'password' }
-      await registerUser(server.url, user.username, user.password)
-      const userAccessToken = await userLogin(server, user)
+      await server.users.register(user)
+      const userToken = await server.login.getAccessToken(user)
 
-      const videoAttributes = { fixture: 'video_short2.webm' }
+      const attributes = { fixture: 'video_short2.webm' }
       for (let i = 0; i < 5; i++) {
-        await uploadVideo(server.url, userAccessToken, videoAttributes)
+        await command.upload({ token: userToken, attributes })
       }
 
-      await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy')
+      await command.upload({ token: userToken, attributes, expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
     })
 
     it('Should fail with a registered user having too many videos with resumable upload', async function () {
       this.timeout(30000)
 
       const user = { username: 'registered' + randomInt(1, 1500), password: 'password' }
-      await registerUser(server.url, user.username, user.password)
-      const userAccessToken = await userLogin(server, user)
+      await server.users.register(user)
+      const userToken = await server.login.getAccessToken(user)
 
-      const videoAttributes = { fixture: 'video_short2.webm' }
+      const attributes = { fixture: 'video_short2.webm' }
       for (let i = 0; i < 5; i++) {
-        await uploadVideo(server.url, userAccessToken, videoAttributes)
+        await command.upload({ token: userToken, attributes })
       }
 
-      await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable')
+      await command.upload({ token: userToken, attributes, expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
     })
 
     it('Should fail to import with HTTP/Torrent/magnet', async function () {
       this.timeout(120000)
 
       const baseAttributes = {
-        channelId: server.videoChannel.id,
+        channelId: server.store.channel.id,
         privacy: VideoPrivacy.PUBLIC
       }
-      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { targetUrl: getGoodVideoUrl() }))
-      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { magnetUri: getMagnetURI() }))
-      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { torrentfile: 'video-720p.torrent' as any }))
+      await server.imports.importVideo({ attributes: { ...baseAttributes, targetUrl: FIXTURE_URLS.goodVideo } })
+      await server.imports.importVideo({ attributes: { ...baseAttributes, magnetUri: FIXTURE_URLS.magnet } })
+      await server.imports.importVideo({ attributes: { ...baseAttributes, torrentfile: 'video-720p.torrent' as any } })
 
       await waitJobs([ server ])
 
-      const res = await getMyVideoImports(server.url, server.accessToken)
+      const { total, data: videoImports } = await server.imports.getMyVideoImports()
+      expect(total).to.equal(3)
 
-      expect(res.body.total).to.equal(3)
-      const videoImports: VideoImport[] = res.body.data
       expect(videoImports).to.have.lengthOf(3)
 
       for (const videoImport of videoImports) {
@@ -106,43 +98,34 @@ describe('Test upload quota', function () {
   describe('When having a daily video quota', function () {
 
     it('Should fail with a user having too many videos daily', async function () {
-      await updateUser({
-        url: server.url,
-        userId: rootId,
-        accessToken: server.accessToken,
-        videoQuotaDaily: 42
-      })
+      await server.users.update({ userId: rootId, videoQuotaDaily: 42 })
 
-      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy')
-      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable')
+      await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
+      await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
     })
   })
 
   describe('When having an absolute and daily video quota', function () {
     it('Should fail if exceeding total quota', async function () {
-      await updateUser({
-        url: server.url,
+      await server.users.update({
         userId: rootId,
-        accessToken: server.accessToken,
         videoQuota: 42,
         videoQuotaDaily: 1024 * 1024 * 1024
       })
 
-      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy')
-      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable')
+      await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
+      await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
     })
 
     it('Should fail if exceeding daily quota', async function () {
-      await updateUser({
-        url: server.url,
+      await server.users.update({
         userId: rootId,
-        accessToken: server.accessToken,
         videoQuota: 1024 * 1024 * 1024,
         videoQuotaDaily: 42
       })
 
-      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy')
-      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable')
+      await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
+      await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
     })
   })
 
index 26d4423f9313d3a545e4a94f9b916a5d69212ca5..17edf5aa1789dd1456a3a52f971cc5c7dadac08f 100644 (file)
@@ -2,35 +2,30 @@
 
 import 'mocha'
 import { io } from 'socket.io-client'
-
 import {
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   cleanupTests,
-  flushAndRunServer,
-  immutableAssign,
+  createSingleServer,
   makeGetRequest,
   makePostBodyRequest,
   makePutBodyRequest,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
   wait
-} from '../../../../shared/extra-utils'
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { UserNotificationSetting, UserNotificationSettingValue } from '../../../../shared/models/users'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+} from '@shared/extra-utils'
+import { HttpStatusCode, UserNotificationSetting, UserNotificationSettingValue } from '@shared/models'
 
 describe('Test user notifications API validators', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
   })
@@ -58,7 +53,7 @@ describe('Test user notifications API validators', function () {
           unread: 'toto'
         },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
 
@@ -66,7 +61,7 @@ describe('Test user notifications API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -75,7 +70,7 @@ describe('Test user notifications API validators', function () {
         url: server.url,
         path,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -91,7 +86,7 @@ describe('Test user notifications API validators', function () {
           ids: [ 'hello' ]
         },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
 
       await makePostBodyRequest({
@@ -101,7 +96,7 @@ describe('Test user notifications API validators', function () {
           ids: [ ]
         },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
 
       await makePostBodyRequest({
@@ -111,7 +106,7 @@ describe('Test user notifications API validators', function () {
           ids: 5
         },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -122,7 +117,7 @@ describe('Test user notifications API validators', function () {
         fields: {
           ids: [ 5 ]
         },
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -134,7 +129,7 @@ describe('Test user notifications API validators', function () {
           ids: [ 5 ]
         },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -146,7 +141,7 @@ describe('Test user notifications API validators', function () {
       await makePostBodyRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -155,7 +150,7 @@ describe('Test user notifications API validators', function () {
         url: server.url,
         path,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -187,32 +182,32 @@ describe('Test user notifications API validators', function () {
         path,
         token: server.accessToken,
         fields: { newVideoFromSubscription: UserNotificationSettingValue.WEB },
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
     it('Should fail with incorrect field values', async function () {
       {
-        const fields = immutableAssign(correctFields, { newCommentOnMyVideo: 15 })
+        const fields = { ...correctFields, newCommentOnMyVideo: 15 }
 
         await makePutBodyRequest({
           url: server.url,
           path,
           token: server.accessToken,
           fields,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       }
 
       {
-        const fields = immutableAssign(correctFields, { newCommentOnMyVideo: 'toto' })
+        const fields = { ...correctFields, newCommentOnMyVideo: 'toto' }
 
         await makePutBodyRequest({
           url: server.url,
           path,
           fields,
           token: server.accessToken,
-          statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
         })
       }
     })
@@ -222,7 +217,7 @@ describe('Test user notifications API validators', function () {
         url: server.url,
         path,
         fields: correctFields,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -232,7 +227,7 @@ describe('Test user notifications API validators', function () {
         path,
         token: server.accessToken,
         fields: correctFields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
index 538201647417a259e3df5194e908442133c6a792..624069c80ba0c2adada7b8c272d54ea78e5588f4 100644 (file)
@@ -1,30 +1,24 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
 import {
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   cleanupTests,
-  createUser,
-  flushAndRunServer,
+  createSingleServer,
   makeDeleteRequest,
   makeGetRequest,
   makePostBodyRequest,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
-
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+  waitJobs
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test user subscriptions API validators', function () {
   const path = '/api/v1/users/me/subscriptions'
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken = ''
 
   // ---------------------------------------------------------------
@@ -32,7 +26,7 @@ describe('Test user subscriptions API validators', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
@@ -40,8 +34,8 @@ describe('Test user subscriptions API validators', function () {
       username: 'user1',
       password: 'my super password'
     }
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-    userAccessToken = await userLogin(server, user)
+    await server.users.create({ username: user.username, password: user.password })
+    userAccessToken = await server.login.getAccessToken(user)
   })
 
   describe('When listing my subscriptions', function () {
@@ -61,7 +55,7 @@ describe('Test user subscriptions API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -70,7 +64,7 @@ describe('Test user subscriptions API validators', function () {
         url: server.url,
         path,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -94,7 +88,7 @@ describe('Test user subscriptions API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -103,7 +97,7 @@ describe('Test user subscriptions API validators', function () {
         url: server.url,
         path,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -114,7 +108,7 @@ describe('Test user subscriptions API validators', function () {
         url: server.url,
         path,
         fields: { uri: 'user1_channel@localhost:' + server.port },
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -124,7 +118,7 @@ describe('Test user subscriptions API validators', function () {
         path,
         token: server.accessToken,
         fields: { uri: 'root' },
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
 
       await makePostBodyRequest({
@@ -132,7 +126,7 @@ describe('Test user subscriptions API validators', function () {
         path,
         token: server.accessToken,
         fields: { uri: 'root@' },
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
 
       await makePostBodyRequest({
@@ -140,7 +134,7 @@ describe('Test user subscriptions API validators', function () {
         path,
         token: server.accessToken,
         fields: { uri: 'root@hello@' },
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -152,7 +146,7 @@ describe('Test user subscriptions API validators', function () {
         path,
         token: server.accessToken,
         fields: { uri: 'user1_channel@localhost:' + server.port },
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
 
       await waitJobs([ server ])
@@ -164,7 +158,7 @@ describe('Test user subscriptions API validators', function () {
       await makeGetRequest({
         url: server.url,
         path: path + '/user1_channel@localhost:' + server.port,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -173,21 +167,21 @@ describe('Test user subscriptions API validators', function () {
         url: server.url,
         path: path + '/root',
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
 
       await makeGetRequest({
         url: server.url,
         path: path + '/root@',
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
 
       await makeGetRequest({
         url: server.url,
         path: path + '/root@hello@',
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -196,7 +190,7 @@ describe('Test user subscriptions API validators', function () {
         url: server.url,
         path: path + '/root1@localhost:' + server.port,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -205,7 +199,7 @@ describe('Test user subscriptions API validators', function () {
         url: server.url,
         path: path + '/user1_channel@localhost:' + server.port,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -217,7 +211,7 @@ describe('Test user subscriptions API validators', function () {
       await makeGetRequest({
         url: server.url,
         path: existPath,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -227,7 +221,7 @@ describe('Test user subscriptions API validators', function () {
         path: existPath,
         query: { uris: 'toto' },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
 
       await makeGetRequest({
@@ -235,7 +229,7 @@ describe('Test user subscriptions API validators', function () {
         path: existPath,
         query: { 'uris[]': 1 },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -245,7 +239,7 @@ describe('Test user subscriptions API validators', function () {
         path: existPath,
         query: { 'uris[]': 'coucou@localhost:' + server.port },
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -255,7 +249,7 @@ describe('Test user subscriptions API validators', function () {
       await makeDeleteRequest({
         url: server.url,
         path: path + '/user1_channel@localhost:' + server.port,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -264,21 +258,21 @@ describe('Test user subscriptions API validators', function () {
         url: server.url,
         path: path + '/root',
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
 
       await makeDeleteRequest({
         url: server.url,
         path: path + '/root@',
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
 
       await makeDeleteRequest({
         url: server.url,
         path: path + '/root@hello@',
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -287,7 +281,7 @@ describe('Test user subscriptions API validators', function () {
         url: server.url,
         path: path + '/root1@localhost:' + server.port,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -296,7 +290,7 @@ describe('Test user subscriptions API validators', function () {
         url: server.url,
         path: path + '/user1_channel@localhost:' + server.port,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
index 70a872ce57f17ac9a92bbf322959f26a8d3edf72..9d8f933db2aa6a2da3cd44a0eeddf9b827f8de58 100644 (file)
@@ -2,43 +2,24 @@
 
 import 'mocha'
 import { omit } from 'lodash'
-import { User, UserRole, VideoCreateResult } from '../../../../shared'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
-  addVideoChannel,
-  blockUser,
   buildAbsoluteFixturePath,
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   cleanupTests,
-  createUser,
-  deleteMe,
-  flushAndRunServer,
-  getMyUserInformation,
-  getMyUserVideoRating,
-  getUserScopedTokens,
-  getUsersList,
-  immutableAssign,
+  createSingleServer,
   killallServers,
   makeGetRequest,
   makePostBodyRequest,
   makePutBodyRequest,
   makeUploadRequest,
-  registerUser,
-  removeUser,
-  renewUserScopedTokens,
-  reRunServer,
-  ServerInfo,
+  MockSmtpServer,
+  PeerTubeServer,
   setAccessTokensToServers,
-  unblockUser,
-  uploadVideo,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
+  UsersCommand
+} from '@shared/extra-utils'
+import { HttpStatusCode, UserAdminFlag, UserRole, VideoCreateResult } from '@shared/models'
 
 describe('Test users API validators', function () {
   const path = '/api/v1/users/'
@@ -46,10 +27,10 @@ describe('Test users API validators', function () {
   let rootId: number
   let moderatorId: number
   let video: VideoCreateResult
-  let server: ServerInfo
-  let serverWithRegistrationDisabled: ServerInfo
-  let userAccessToken = ''
-  let moderatorAccessToken = ''
+  let server: PeerTubeServer
+  let serverWithRegistrationDisabled: PeerTubeServer
+  let userToken = ''
+  let moderatorToken = ''
   let emailPort: number
   let overrideConfig: Object
 
@@ -65,8 +46,8 @@ describe('Test users API validators', function () {
 
     {
       const res = await Promise.all([
-        flushAndRunServer(1, overrideConfig),
-        flushAndRunServer(2)
+        createSingleServer(1, overrideConfig),
+        createSingleServer(2)
       ])
 
       server = res[0]
@@ -76,66 +57,31 @@ describe('Test users API validators', function () {
     }
 
     {
-      const user = {
-        username: 'user1',
-        password: 'my super password'
-      }
-
-      const videoQuota = 42000000
-      await createUser({
-        url: server.url,
-        accessToken: server.accessToken,
-        username: user.username,
-        password: user.password,
-        videoQuota: videoQuota
-      })
-      userAccessToken = await userLogin(server, user)
+      const user = { username: 'user1' }
+      await server.users.create({ ...user })
+      userToken = await server.login.getAccessToken(user)
     }
 
     {
-      const moderator = {
-        username: 'moderator1',
-        password: 'super password'
-      }
-
-      await createUser({
-        url: server.url,
-        accessToken: server.accessToken,
-        username: moderator.username,
-        password: moderator.password,
-        role: UserRole.MODERATOR
-      })
-
-      moderatorAccessToken = await userLogin(server, moderator)
+      const moderator = { username: 'moderator1' }
+      await server.users.create({ ...moderator, role: UserRole.MODERATOR })
+      moderatorToken = await server.login.getAccessToken(moderator)
     }
 
     {
-      const moderator = {
-        username: 'moderator2',
-        password: 'super password'
-      }
-
-      await createUser({
-        url: server.url,
-        accessToken: server.accessToken,
-        username: moderator.username,
-        password: moderator.password,
-        role: UserRole.MODERATOR
-      })
+      const moderator = { username: 'moderator2' }
+      await server.users.create({ ...moderator, role: UserRole.MODERATOR })
     }
 
     {
-      const res = await uploadVideo(server.url, server.accessToken, {})
-      video = res.body.video
+      video = await server.videos.upload()
     }
 
     {
-      const res = await getUsersList(server.url, server.accessToken)
-      const users: User[] = res.body.data
-
-      userId = users.find(u => u.username === 'user1').id
-      rootId = users.find(u => u.username === 'root').id
-      moderatorId = users.find(u => u.username === 'moderator2').id
+      const { data } = await server.users.list()
+      userId = data.find(u => u.username === 'user1').id
+      rootId = data.find(u => u.username === 'root').id
+      moderatorId = data.find(u => u.username === 'moderator2').id
     }
   })
 
@@ -156,7 +102,7 @@ describe('Test users API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -164,8 +110,8 @@ describe('Test users API validators', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        token: userToken,
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
   })
@@ -182,25 +128,25 @@ describe('Test users API validators', function () {
     }
 
     it('Should fail with a too small username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: '' })
+      const fields = { ...baseCorrectParams, username: '' }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a too long username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: 'super'.repeat(50) })
+      const fields = { ...baseCorrectParams, username: 'super'.repeat(50) }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a not lowercase username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: 'Toto' })
+      const fields = { ...baseCorrectParams, username: 'Toto' }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with an incorrect username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: 'my username' })
+      const fields = { ...baseCorrectParams, username: 'my username' }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -212,25 +158,25 @@ describe('Test users API validators', function () {
     })
 
     it('Should fail with an invalid email', async function () {
-      const fields = immutableAssign(baseCorrectParams, { email: 'test_example.com' })
+      const fields = { ...baseCorrectParams, email: 'test_example.com' }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a too small password', async function () {
-      const fields = immutableAssign(baseCorrectParams, { password: 'bla' })
+      const fields = { ...baseCorrectParams, password: 'bla' }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a too long password', async function () {
-      const fields = immutableAssign(baseCorrectParams, { password: 'super'.repeat(61) })
+      const fields = { ...baseCorrectParams, password: 'super'.repeat(61) }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with empty password and no smtp configured', async function () {
-      const fields = immutableAssign(baseCorrectParams, { password: '' })
+      const fields = { ...baseCorrectParams, password: '' }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -238,33 +184,37 @@ describe('Test users API validators', function () {
     it('Should succeed with no password on a server with smtp enabled', async function () {
       this.timeout(20000)
 
-      killallServers([ server ])
+      await killallServers([ server ])
+
+      const config = {
+        ...overrideConfig,
 
-      const config = immutableAssign(overrideConfig, {
         smtp: {
           hostname: 'localhost',
           port: emailPort
         }
-      })
-      await reRunServer(server, config)
+      }
+      await server.run(config)
+
+      const fields = {
+        ...baseCorrectParams,
 
-      const fields = immutableAssign(baseCorrectParams, {
         password: '',
         username: 'create_password',
         email: 'create_password@example.com'
-      })
+      }
 
       await makePostBodyRequest({
         url: server.url,
         path: path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
 
     it('Should fail with invalid admin flags', async function () {
-      const fields = immutableAssign(baseCorrectParams, { adminFlags: 'toto' })
+      const fields = { ...baseCorrectParams, adminFlags: 'toto' }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -275,31 +225,31 @@ describe('Test users API validators', function () {
         path,
         token: 'super token',
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
     it('Should fail if we add a user with the same username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: 'user1' })
+      const fields = { ...baseCorrectParams, username: 'user1' }
 
       await makePostBodyRequest({
         url: server.url,
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
 
     it('Should fail if we add a user with the same email', async function () {
-      const fields = immutableAssign(baseCorrectParams, { email: 'user1@example.com' })
+      const fields = { ...baseCorrectParams, email: 'user1@example.com' }
 
       await makePostBodyRequest({
         url: server.url,
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
 
@@ -316,13 +266,13 @@ describe('Test users API validators', function () {
     })
 
     it('Should fail with an invalid videoQuota', async function () {
-      const fields = immutableAssign(baseCorrectParams, { videoQuota: -5 })
+      const fields = { ...baseCorrectParams, videoQuota: -5 }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with an invalid videoQuotaDaily', async function () {
-      const fields = immutableAssign(baseCorrectParams, { videoQuotaDaily: -7 })
+      const fields = { ...baseCorrectParams, videoQuotaDaily: -7 }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -334,46 +284,46 @@ describe('Test users API validators', function () {
     })
 
     it('Should fail with an invalid user role', async function () {
-      const fields = immutableAssign(baseCorrectParams, { role: 88989 })
+      const fields = { ...baseCorrectParams, role: 88989 }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a "peertube" username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: 'peertube' })
+      const fields = { ...baseCorrectParams, username: 'peertube' }
 
       await makePostBodyRequest({
         url: server.url,
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
 
     it('Should fail to create a moderator or an admin with a moderator', async function () {
       for (const role of [ UserRole.MODERATOR, UserRole.ADMINISTRATOR ]) {
-        const fields = immutableAssign(baseCorrectParams, { role })
+        const fields = { ...baseCorrectParams, role }
 
         await makePostBodyRequest({
           url: server.url,
           path,
-          token: moderatorAccessToken,
+          token: moderatorToken,
           fields,
-          statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+          expectedStatus: HttpStatusCode.FORBIDDEN_403
         })
       }
     })
 
     it('Should succeed to create a user with a moderator', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: 'a4656', email: 'a4656@example.com', role: UserRole.USER })
+      const fields = { ...baseCorrectParams, username: 'a4656', email: 'a4656@example.com', role: UserRole.USER }
 
       await makePostBodyRequest({
         url: server.url,
         path,
-        token: moderatorAccessToken,
+        token: moderatorToken,
         fields,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
 
@@ -383,16 +333,13 @@ describe('Test users API validators', function () {
         path,
         token: server.accessToken,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
 
     it('Should fail with a non admin user', async function () {
-      const user = {
-        username: 'user1',
-        password: 'my super password'
-      }
-      userAccessToken = await userLogin(server, user)
+      const user = { username: 'user1' }
+      userToken = await server.login.getAccessToken(user)
 
       const fields = {
         username: 'user3',
@@ -400,11 +347,12 @@ describe('Test users API validators', function () {
         password: 'my super password',
         videoQuota: 42000000
       }
-      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, statusCodeExpected: HttpStatusCode.FORBIDDEN_403 })
+      await makePostBodyRequest({ url: server.url, path, token: userToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
   })
 
   describe('When updating my account', function () {
+
     it('Should fail with an invalid email attribute', async function () {
       const fields = {
         email: 'blabla'
@@ -415,29 +363,29 @@ describe('Test users API validators', function () {
 
     it('Should fail with a too small password', async function () {
       const fields = {
-        currentPassword: 'my super password',
+        currentPassword: 'password',
         password: 'bla'
       }
 
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail with a too long password', async function () {
       const fields = {
-        currentPassword: 'my super password',
+        currentPassword: 'password',
         password: 'super'.repeat(61)
       }
 
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail without the current password', async function () {
       const fields = {
-        currentPassword: 'my super password',
+        currentPassword: 'password',
         password: 'super'.repeat(61)
       }
 
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail with an invalid current password', async function () {
@@ -449,9 +397,9 @@ describe('Test users API validators', function () {
       await makePutBodyRequest({
         url: server.url,
         path: path + 'me',
-        token: userAccessToken,
+        token: userToken,
         fields,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -460,7 +408,7 @@ describe('Test users API validators', function () {
         nsfwPolicy: 'hello'
       }
 
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail with an invalid autoPlayVideo attribute', async function () {
@@ -468,7 +416,7 @@ describe('Test users API validators', function () {
         autoPlayVideo: -1
       }
 
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail with an invalid autoPlayNextVideo attribute', async function () {
@@ -476,7 +424,7 @@ describe('Test users API validators', function () {
         autoPlayNextVideo: -1
       }
 
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail with an invalid videosHistoryEnabled attribute', async function () {
@@ -484,12 +432,12 @@ describe('Test users API validators', function () {
         videosHistoryEnabled: -1
       }
 
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail with an non authenticated user', async function () {
       const fields = {
-        currentPassword: 'my super password',
+        currentPassword: 'password',
         password: 'my super password'
       }
 
@@ -498,7 +446,7 @@ describe('Test users API validators', function () {
         path: path + 'me',
         token: 'super token',
         fields,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -507,7 +455,7 @@ describe('Test users API validators', function () {
         description: 'super'.repeat(201)
       }
 
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail with an invalid videoLanguages attribute', async function () {
@@ -516,7 +464,7 @@ describe('Test users API validators', function () {
           videoLanguages: 'toto'
         }
 
-        await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+        await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
       }
 
       {
@@ -529,18 +477,18 @@ describe('Test users API validators', function () {
           videoLanguages: languages
         }
 
-        await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+        await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
       }
     })
 
     it('Should fail with an invalid theme', async function () {
       const fields = { theme: 'invalid' }
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail with an unknown theme', async function () {
       const fields = { theme: 'peertube-theme-unknown' }
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail with an invalid noInstanceConfigWarningModal attribute', async function () {
@@ -548,7 +496,7 @@ describe('Test users API validators', function () {
         noInstanceConfigWarningModal: -1
       }
 
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should fail with an invalid noWelcomeModal attribute', async function () {
@@ -556,12 +504,12 @@ describe('Test users API validators', function () {
         noWelcomeModal: -1
       }
 
-      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
     })
 
     it('Should succeed to change password with the correct params', async function () {
       const fields = {
-        currentPassword: 'my super password',
+        currentPassword: 'password',
         password: 'my super password',
         nsfwPolicy: 'blur',
         autoPlayVideo: false,
@@ -574,9 +522,9 @@ describe('Test users API validators', function () {
       await makePutBodyRequest({
         url: server.url,
         path: path + 'me',
-        token: userAccessToken,
+        token: userToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
 
@@ -589,9 +537,9 @@ describe('Test users API validators', function () {
       await makePutBodyRequest({
         url: server.url,
         path: path + 'me',
-        token: userAccessToken,
+        token: userToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -623,7 +571,7 @@ describe('Test users API validators', function () {
         path: path + '/me/avatar/pick',
         fields,
         attaches,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -638,7 +586,7 @@ describe('Test users API validators', function () {
         token: server.accessToken,
         fields,
         attaches,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -646,28 +594,28 @@ describe('Test users API validators', function () {
   describe('When managing my scoped tokens', function () {
 
     it('Should fail to get my scoped tokens with an non authenticated user', async function () {
-      await getUserScopedTokens(server.url, null, HttpStatusCode.UNAUTHORIZED_401)
+      await server.users.getMyScopedTokens({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail to get my scoped tokens with a bad token', async function () {
-      await getUserScopedTokens(server.url, 'bad', HttpStatusCode.UNAUTHORIZED_401)
+      await server.users.getMyScopedTokens({ token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
 
     })
 
     it('Should succeed to get my scoped tokens', async function () {
-      await getUserScopedTokens(server.url, server.accessToken)
+      await server.users.getMyScopedTokens()
     })
 
     it('Should fail to renew my scoped tokens with an non authenticated user', async function () {
-      await renewUserScopedTokens(server.url, null, HttpStatusCode.UNAUTHORIZED_401)
+      await server.users.renewMyScopedTokens({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail to renew my scoped tokens with a bad token', async function () {
-      await renewUserScopedTokens(server.url, 'bad', HttpStatusCode.UNAUTHORIZED_401)
+      await server.users.renewMyScopedTokens({ token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should succeed to renew my scoped tokens', async function () {
-      await renewUserScopedTokens(server.url, server.accessToken)
+      await server.users.renewMyScopedTokens()
     })
   })
 
@@ -678,16 +626,16 @@ describe('Test users API validators', function () {
         url: server.url,
         path: path + userId,
         token: 'super token',
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
     it('Should fail with a non admin user', async function () {
-      await makeGetRequest({ url: server.url, path, token: userAccessToken, statusCodeExpected: HttpStatusCode.FORBIDDEN_403 })
+      await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should succeed with the correct params', async function () {
-      await makeGetRequest({ url: server.url, path: path + userId, token: server.accessToken, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, path: path + userId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -727,7 +675,7 @@ describe('Test users API validators', function () {
 
     it('Should fail with a too small password', async function () {
       const fields = {
-        currentPassword: 'my super password',
+        currentPassword: 'password',
         password: 'bla'
       }
 
@@ -736,7 +684,7 @@ describe('Test users API validators', function () {
 
     it('Should fail with a too long password', async function () {
       const fields = {
-        currentPassword: 'my super password',
+        currentPassword: 'password',
         password: 'super'.repeat(61)
       }
 
@@ -753,7 +701,7 @@ describe('Test users API validators', function () {
         path: path + userId,
         token: 'super token',
         fields,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -779,9 +727,9 @@ describe('Test users API validators', function () {
       await makePutBodyRequest({
         url: server.url,
         path: path + moderatorId,
-        token: moderatorAccessToken,
+        token: moderatorToken,
         fields,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -793,9 +741,9 @@ describe('Test users API validators', function () {
       await makePutBodyRequest({
         url: server.url,
         path: path + userId,
-        token: moderatorAccessToken,
+        token: moderatorToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
 
@@ -812,38 +760,44 @@ describe('Test users API validators', function () {
         path: path + userId,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
 
   describe('When getting my information', function () {
     it('Should fail with a non authenticated user', async function () {
-      await getMyUserInformation(server.url, 'fake_token', HttpStatusCode.UNAUTHORIZED_401)
+      await server.users.getMyInfo({ token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should success with the correct parameters', async function () {
-      await getMyUserInformation(server.url, userAccessToken)
+      await server.users.getMyInfo({ token: userToken })
     })
   })
 
   describe('When getting my video rating', function () {
+    let command: UsersCommand
+
+    before(function () {
+      command = server.users
+    })
+
     it('Should fail with a non authenticated user', async function () {
-      await getMyUserVideoRating(server.url, 'fake_token', video.id, HttpStatusCode.UNAUTHORIZED_401)
+      await command.getMyRating({ token: 'fake_token', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with an incorrect video uuid', async function () {
-      await getMyUserVideoRating(server.url, server.accessToken, 'blabla', HttpStatusCode.BAD_REQUEST_400)
+      await command.getMyRating({ videoId: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should fail with an unknown video', async function () {
-      await getMyUserVideoRating(server.url, server.accessToken, '4da6fde3-88f7-4d16-b119-108df5630b06', HttpStatusCode.NOT_FOUND_404)
+      await command.getMyRating({ videoId: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should succeed with the correct parameters', async function () {
-      await getMyUserVideoRating(server.url, server.accessToken, video.id)
-      await getMyUserVideoRating(server.url, server.accessToken, video.uuid)
-      await getMyUserVideoRating(server.url, server.accessToken, video.shortUUID)
+      await command.getMyRating({ videoId: video.id })
+      await command.getMyRating({ videoId: video.uuid })
+      await command.getMyRating({ videoId: video.shortUUID })
     })
   })
 
@@ -851,80 +805,93 @@ describe('Test users API validators', function () {
     const path = '/api/v1/accounts/user1/ratings'
 
     it('Should fail with a bad start pagination', async function () {
-      await checkBadStartPagination(server.url, path, userAccessToken)
+      await checkBadStartPagination(server.url, path, userToken)
     })
 
     it('Should fail with a bad count pagination', async function () {
-      await checkBadCountPagination(server.url, path, userAccessToken)
+      await checkBadCountPagination(server.url, path, userToken)
     })
 
     it('Should fail with an incorrect sort', async function () {
-      await checkBadSortPagination(server.url, path, userAccessToken)
+      await checkBadSortPagination(server.url, path, userToken)
     })
 
     it('Should fail with a unauthenticated user', async function () {
-      await makeGetRequest({ url: server.url, path, statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a another user', async function () {
-      await makeGetRequest({ url: server.url, path, token: server.accessToken, statusCodeExpected: HttpStatusCode.FORBIDDEN_403 })
+      await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with a bad type', async function () {
       await makeGetRequest({
         url: server.url,
         path,
-        token: userAccessToken,
+        token: userToken,
         query: { rating: 'toto ' },
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
     it('Should succeed with the correct params', async function () {
-      await makeGetRequest({ url: server.url, path, token: userAccessToken, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
   describe('When blocking/unblocking/removing user', function () {
+
     it('Should fail with an incorrect id', async function () {
-      await removeUser(server.url, 'blabla', server.accessToken, HttpStatusCode.BAD_REQUEST_400)
-      await blockUser(server.url, 'blabla', server.accessToken, HttpStatusCode.BAD_REQUEST_400)
-      await unblockUser(server.url, 'blabla', server.accessToken, HttpStatusCode.BAD_REQUEST_400)
+      const options = { userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
+
+      await server.users.remove(options)
+      await server.users.banUser({ userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      await server.users.unbanUser({ userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should fail with the root user', async function () {
-      await removeUser(server.url, rootId, server.accessToken, HttpStatusCode.BAD_REQUEST_400)
-      await blockUser(server.url, rootId, server.accessToken, HttpStatusCode.BAD_REQUEST_400)
-      await unblockUser(server.url, rootId, server.accessToken, HttpStatusCode.BAD_REQUEST_400)
+      const options = { userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
+
+      await server.users.remove(options)
+      await server.users.banUser(options)
+      await server.users.unbanUser(options)
     })
 
     it('Should return 404 with a non existing id', async function () {
-      await removeUser(server.url, 4545454, server.accessToken, HttpStatusCode.NOT_FOUND_404)
-      await blockUser(server.url, 4545454, server.accessToken, HttpStatusCode.NOT_FOUND_404)
-      await unblockUser(server.url, 4545454, server.accessToken, HttpStatusCode.NOT_FOUND_404)
+      const options = { userId: 4545454, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
+
+      await server.users.remove(options)
+      await server.users.banUser(options)
+      await server.users.unbanUser(options)
     })
 
     it('Should fail with a non admin user', async function () {
-      await removeUser(server.url, userId, userAccessToken, HttpStatusCode.FORBIDDEN_403)
-      await blockUser(server.url, userId, userAccessToken, HttpStatusCode.FORBIDDEN_403)
-      await unblockUser(server.url, userId, userAccessToken, HttpStatusCode.FORBIDDEN_403)
+      const options = { userId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }
+
+      await server.users.remove(options)
+      await server.users.banUser(options)
+      await server.users.unbanUser(options)
     })
 
     it('Should fail on a moderator with a moderator', async function () {
-      await removeUser(server.url, moderatorId, moderatorAccessToken, HttpStatusCode.FORBIDDEN_403)
-      await blockUser(server.url, moderatorId, moderatorAccessToken, HttpStatusCode.FORBIDDEN_403)
-      await unblockUser(server.url, moderatorId, moderatorAccessToken, HttpStatusCode.FORBIDDEN_403)
+      const options = { userId: moderatorId, token: moderatorToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }
+
+      await server.users.remove(options)
+      await server.users.banUser(options)
+      await server.users.unbanUser(options)
     })
 
     it('Should succeed on a user with a moderator', async function () {
-      await blockUser(server.url, userId, moderatorAccessToken)
-      await unblockUser(server.url, userId, moderatorAccessToken)
+      const options = { userId, token: moderatorToken }
+
+      await server.users.banUser(options)
+      await server.users.unbanUser(options)
     })
   })
 
   describe('When deleting our account', function () {
     it('Should fail with with the root account', async function () {
-      await deleteMe(server.url, server.accessToken, HttpStatusCode.BAD_REQUEST_400)
+      await server.users.deleteMe({ expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
   })
 
@@ -938,19 +905,19 @@ describe('Test users API validators', function () {
     }
 
     it('Should fail with a too small username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: '' })
+      const fields = { ...baseCorrectParams, username: '' }
 
       await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
     })
 
     it('Should fail with a too long username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: 'super'.repeat(50) })
+      const fields = { ...baseCorrectParams, username: 'super'.repeat(50) }
 
       await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
     })
 
     it('Should fail with an incorrect username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: 'my username' })
+      const fields = { ...baseCorrectParams, username: 'my username' }
 
       await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
     })
@@ -962,108 +929,108 @@ describe('Test users API validators', function () {
     })
 
     it('Should fail with an invalid email', async function () {
-      const fields = immutableAssign(baseCorrectParams, { email: 'test_example.com' })
+      const fields = { ...baseCorrectParams, email: 'test_example.com' }
 
       await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
     })
 
     it('Should fail with a too small password', async function () {
-      const fields = immutableAssign(baseCorrectParams, { password: 'bla' })
+      const fields = { ...baseCorrectParams, password: 'bla' }
 
       await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
     })
 
     it('Should fail with a too long password', async function () {
-      const fields = immutableAssign(baseCorrectParams, { password: 'super'.repeat(61) })
+      const fields = { ...baseCorrectParams, password: 'super'.repeat(61) }
 
       await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
     })
 
     it('Should fail if we register a user with the same username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: 'root' })
+      const fields = { ...baseCorrectParams, username: 'root' }
 
       await makePostBodyRequest({
         url: server.url,
         path: registrationPath,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
 
     it('Should fail with a "peertube" username', async function () {
-      const fields = immutableAssign(baseCorrectParams, { username: 'peertube' })
+      const fields = { ...baseCorrectParams, username: 'peertube' }
 
       await makePostBodyRequest({
         url: server.url,
         path: registrationPath,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
 
     it('Should fail if we register a user with the same email', async function () {
-      const fields = immutableAssign(baseCorrectParams, { email: 'admin' + server.internalServerNumber + '@example.com' })
+      const fields = { ...baseCorrectParams, email: 'admin' + server.internalServerNumber + '@example.com' }
 
       await makePostBodyRequest({
         url: server.url,
         path: registrationPath,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
 
     it('Should fail with a bad display name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { displayName: 'a'.repeat(150) })
+      const fields = { ...baseCorrectParams, displayName: 'a'.repeat(150) }
 
       await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad channel name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { channel: { name: '[]azf', displayName: 'toto' } })
+      const fields = { ...baseCorrectParams, channel: { name: '[]azf', displayName: 'toto' } }
 
       await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad channel display name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { channel: { name: 'toto', displayName: '' } })
+      const fields = { ...baseCorrectParams, channel: { name: 'toto', displayName: '' } }
 
       await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
     })
 
     it('Should fail with a channel name that is the same as username', async function () {
       const source = { username: 'super_user', channel: { name: 'super_user', displayName: 'display name' } }
-      const fields = immutableAssign(baseCorrectParams, source)
+      const fields = { ...baseCorrectParams, ...source }
 
       await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
     })
 
     it('Should fail with an existing channel', async function () {
-      const videoChannelAttributesArg = { name: 'existing_channel', displayName: 'hello', description: 'super description' }
-      await addVideoChannel(server.url, server.accessToken, videoChannelAttributesArg)
+      const attributes = { name: 'existing_channel', displayName: 'hello', description: 'super description' }
+      await server.channels.create({ attributes })
 
-      const fields = immutableAssign(baseCorrectParams, { channel: { name: 'existing_channel', displayName: 'toto' } })
+      const fields = { ...baseCorrectParams, channel: { name: 'existing_channel', displayName: 'toto' } }
 
       await makePostBodyRequest({
         url: server.url,
         path: registrationPath,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
 
     it('Should succeed with the correct params', async function () {
-      const fields = immutableAssign(baseCorrectParams, { channel: { name: 'super_channel', displayName: 'toto' } })
+      const fields = { ...baseCorrectParams, channel: { name: 'super_channel', displayName: 'toto' } }
 
       await makePostBodyRequest({
         url: server.url,
         path: registrationPath,
         token: server.accessToken,
         fields: fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
 
@@ -1079,14 +1046,14 @@ describe('Test users API validators', function () {
         path: registrationPath,
         token: serverWithRegistrationDisabled.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
   })
 
   describe('When registering multiple users on a server with users limit', function () {
     it('Should fail when after 3 registrations', async function () {
-      await registerUser(server.url, 'user42', 'super password', HttpStatusCode.FORBIDDEN_403)
+      await server.users.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
   })
 
@@ -1113,7 +1080,7 @@ describe('Test users API validators', function () {
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -1141,7 +1108,7 @@ describe('Test users API validators', function () {
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
index ce7f5fa17816327f25e06d2da0a3e0cd70866e47..1f926d227574149c4c57f6f63f22c43cc5be171d 100644 (file)
@@ -1,46 +1,37 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
+import { expect } from 'chai'
 import {
+  BlacklistCommand,
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   cleanupTests,
-  createUser,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getBlacklistedVideosList,
-  getVideo,
-  getVideoWithToken,
   makePostBodyRequest,
   makePutBodyRequest,
-  removeVideoFromBlacklist,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  uploadVideo,
-  userLogin,
   waitJobs
-} from '../../../../shared/extra-utils'
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { VideoBlacklistType, VideoDetails } from '../../../../shared/models/videos'
-import { expect } from 'chai'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoBlacklistType } from '@shared/models'
 
 describe('Test video blacklist API validators', function () {
-  let servers: ServerInfo[]
-  let notBlacklistedVideoId: number
+  let servers: PeerTubeServer[]
+  let notBlacklistedVideoId: string
   let remoteVideoUUID: string
   let userAccessToken1 = ''
   let userAccessToken2 = ''
+  let command: BlacklistCommand
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
     await doubleFollow(servers[0], servers[1])
@@ -48,40 +39,41 @@ describe('Test video blacklist API validators', function () {
     {
       const username = 'user1'
       const password = 'my super password'
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: username, password: password })
-      userAccessToken1 = await userLogin(servers[0], { username, password })
+      await servers[0].users.create({ username: username, password: password })
+      userAccessToken1 = await servers[0].login.getAccessToken({ username, password })
     }
 
     {
       const username = 'user2'
       const password = 'my super password'
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: username, password: password })
-      userAccessToken2 = await userLogin(servers[0], { username, password })
+      await servers[0].users.create({ username: username, password: password })
+      userAccessToken2 = await servers[0].login.getAccessToken({ username, password })
     }
 
     {
-      const res = await uploadVideo(servers[0].url, userAccessToken1, {})
-      servers[0].video = res.body.video
+      servers[0].store.videoCreated = await servers[0].videos.upload({ token: userAccessToken1 })
     }
 
     {
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, {})
-      notBlacklistedVideoId = res.body.video.uuid
+      const { uuid } = await servers[0].videos.upload()
+      notBlacklistedVideoId = uuid
     }
 
     {
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, {})
-      remoteVideoUUID = res.body.video.uuid
+      const { uuid } = await servers[1].videos.upload()
+      remoteVideoUUID = uuid
     }
 
     await waitJobs(servers)
+
+    command = servers[0].blacklist
   })
 
   describe('When adding a video in blacklist', function () {
     const basePath = '/api/v1/videos/'
 
     it('Should fail with nothing', async function () {
-      const path = basePath + servers[0].video + '/blacklist'
+      const path = basePath + servers[0].store.videoCreated + '/blacklist'
       const fields = {}
       await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields })
     })
@@ -93,25 +85,25 @@ describe('Test video blacklist API validators', function () {
     })
 
     it('Should fail with a non authenticated user', async function () {
-      const path = basePath + servers[0].video + '/blacklist'
+      const path = basePath + servers[0].store.videoCreated + '/blacklist'
       const fields = {}
-      await makePostBodyRequest({ url: servers[0].url, path, token: 'hello', fields, statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makePostBodyRequest({ url: servers[0].url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a non admin user', async function () {
-      const path = basePath + servers[0].video + '/blacklist'
+      const path = basePath + servers[0].store.videoCreated + '/blacklist'
       const fields = {}
       await makePostBodyRequest({
         url: servers[0].url,
         path,
         token: userAccessToken2,
         fields,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
     it('Should fail with an invalid reason', async function () {
-      const path = basePath + servers[0].video.uuid + '/blacklist'
+      const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist'
       const fields = { reason: 'a'.repeat(305) }
 
       await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields })
@@ -126,12 +118,12 @@ describe('Test video blacklist API validators', function () {
         path,
         token: servers[0].accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
 
     it('Should succeed with the correct params', async function () {
-      const path = basePath + servers[0].video.uuid + '/blacklist'
+      const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist'
       const fields = {}
 
       await makePostBodyRequest({
@@ -139,7 +131,7 @@ describe('Test video blacklist API validators', function () {
         path,
         token: servers[0].accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -161,37 +153,37 @@ describe('Test video blacklist API validators', function () {
         path,
         token: servers[0].accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
     it('Should fail with a non authenticated user', async function () {
-      const path = basePath + servers[0].video + '/blacklist'
+      const path = basePath + servers[0].store.videoCreated + '/blacklist'
       const fields = {}
-      await makePutBodyRequest({ url: servers[0].url, path, token: 'hello', fields, statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makePutBodyRequest({ url: servers[0].url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a non admin user', async function () {
-      const path = basePath + servers[0].video + '/blacklist'
+      const path = basePath + servers[0].store.videoCreated + '/blacklist'
       const fields = {}
       await makePutBodyRequest({
         url: servers[0].url,
         path,
         token: userAccessToken2,
         fields,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
     it('Should fail with an invalid reason', async function () {
-      const path = basePath + servers[0].video.uuid + '/blacklist'
+      const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist'
       const fields = { reason: 'a'.repeat(305) }
 
       await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields })
     })
 
     it('Should succeed with the correct params', async function () {
-      const path = basePath + servers[0].video.shortUUID + '/blacklist'
+      const path = basePath + servers[0].store.videoCreated.shortUUID + '/blacklist'
       const fields = { reason: 'hello' }
 
       await makePutBodyRequest({
@@ -199,7 +191,7 @@ describe('Test video blacklist API validators', function () {
         path,
         token: servers[0].accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -207,52 +199,61 @@ describe('Test video blacklist API validators', function () {
   describe('When getting blacklisted video', function () {
 
     it('Should fail with a non authenticated user', async function () {
-      await getVideo(servers[0].url, servers[0].video.uuid, HttpStatusCode.UNAUTHORIZED_401)
+      await servers[0].videos.get({ id: servers[0].store.videoCreated.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with another user', async function () {
-      await getVideoWithToken(servers[0].url, userAccessToken2, servers[0].video.uuid, HttpStatusCode.FORBIDDEN_403)
+      await servers[0].videos.getWithToken({
+        token: userAccessToken2,
+        id: servers[0].store.videoCreated.uuid,
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
     })
 
     it('Should succeed with the owner authenticated user', async function () {
-      const res = await getVideoWithToken(servers[0].url, userAccessToken1, servers[0].video.uuid, HttpStatusCode.OK_200)
-      const video: VideoDetails = res.body
-
+      const video = await servers[0].videos.getWithToken({ token: userAccessToken1, id: servers[0].store.videoCreated.uuid })
       expect(video.blacklisted).to.be.true
     })
 
     it('Should succeed with an admin', async function () {
-      const video = servers[0].video
+      const video = servers[0].store.videoCreated
 
       for (const id of [ video.id, video.uuid, video.shortUUID ]) {
-        const res = await getVideoWithToken(servers[0].url, servers[0].accessToken, id, HttpStatusCode.OK_200)
-        const video: VideoDetails = res.body
-
+        const video = await servers[0].videos.getWithToken({ id, expectedStatus: HttpStatusCode.OK_200 })
         expect(video.blacklisted).to.be.true
       }
     })
   })
 
   describe('When removing a video in blacklist', function () {
+
     it('Should fail with a non authenticated user', async function () {
-      await removeVideoFromBlacklist(servers[0].url, 'fake token', servers[0].video.uuid, HttpStatusCode.UNAUTHORIZED_401)
+      await command.remove({
+        token: 'fake token',
+        videoId: servers[0].store.videoCreated.uuid,
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
+      })
     })
 
     it('Should fail with a non admin user', async function () {
-      await removeVideoFromBlacklist(servers[0].url, userAccessToken2, servers[0].video.uuid, HttpStatusCode.FORBIDDEN_403)
+      await command.remove({
+        token: userAccessToken2,
+        videoId: servers[0].store.videoCreated.uuid,
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
     })
 
     it('Should fail with an incorrect id', async function () {
-      await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, 'hello', HttpStatusCode.BAD_REQUEST_400)
+      await command.remove({ videoId: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should fail with a not blacklisted video', async function () {
       // The video was not added to the blacklist so it should fail
-      await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, notBlacklistedVideoId, HttpStatusCode.NOT_FOUND_404)
+      await command.remove({ videoId: notBlacklistedVideoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should succeed with the correct params', async function () {
-      await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, servers[0].video.uuid, HttpStatusCode.NO_CONTENT_204)
+      await command.remove({ videoId: servers[0].store.videoCreated.uuid, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
     })
   })
 
@@ -260,11 +261,11 @@ describe('Test video blacklist API validators', function () {
     const basePath = '/api/v1/videos/blacklist/'
 
     it('Should fail with a non authenticated user', async function () {
-      await getBlacklistedVideosList({ url: servers[0].url, token: 'fake token', specialStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      await servers[0].blacklist.list({ token: 'fake token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a non admin user', async function () {
-      await getBlacklistedVideosList({ url: servers[0].url, token: userAccessToken2, specialStatus: HttpStatusCode.FORBIDDEN_403 })
+      await servers[0].blacklist.list({ token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with a bad start pagination', async function () {
@@ -280,16 +281,11 @@ describe('Test video blacklist API validators', function () {
     })
 
     it('Should fail with an invalid type', async function () {
-      await getBlacklistedVideosList({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        type: 0,
-        specialStatus: HttpStatusCode.BAD_REQUEST_400
-      })
+      await servers[0].blacklist.list({ type: 0, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should succeed with the correct parameters', async function () {
-      await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, type: VideoBlacklistType.MANUAL })
+      await servers[0].blacklist.list({ type: VideoBlacklistType.MANUAL })
     })
   })
 
index c0595c04d87a78e7a98d7c0e74ceec2e7af0ef87..90f429314543e7953b279541e2e96e51d190e115 100644 (file)
@@ -1,27 +1,22 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { VideoCreateResult } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   buildAbsoluteFixturePath,
   cleanupTests,
-  createUser,
-  flushAndRunServer,
+  createSingleServer,
   makeDeleteRequest,
   makeGetRequest,
   makeUploadRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions'
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoCreateResult } from '@shared/models'
 
 describe('Test video captions API validator', function () {
   const path = '/api/v1/videos/'
 
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken: string
   let video: VideoCreateResult
 
@@ -30,22 +25,19 @@ describe('Test video captions API validator', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
-    {
-      const res = await uploadVideo(server.url, server.accessToken, {})
-      video = res.body.video
-    }
+    video = await server.videos.upload()
 
     {
       const user = {
         username: 'user1',
         password: 'my super password'
       }
-      await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-      userAccessToken = await userLogin(server, user)
+      await server.users.create({ username: user.username, password: user.password })
+      userAccessToken = await server.login.getAccessToken(user)
     }
   })
 
@@ -74,7 +66,7 @@ describe('Test video captions API validator', function () {
         token: server.accessToken,
         fields,
         attaches,
-        statusCodeExpected: 404
+        expectedStatus: 404
       })
     })
 
@@ -110,7 +102,7 @@ describe('Test video captions API validator', function () {
         path: captionPath,
         fields,
         attaches,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -123,7 +115,7 @@ describe('Test video captions API validator', function () {
         token: 'blabla',
         fields,
         attaches,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -141,7 +133,7 @@ describe('Test video captions API validator', function () {
     //     token: server.accessToken,
     //     fields,
     //     attaches,
-    //     statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+    //     expectedStatus: HttpStatusCode.BAD_REQUEST_400
     //   })
     // })
 
@@ -154,14 +146,12 @@ describe('Test video captions API validator', function () {
     //     videoId: video.uuid,
     //     fixture: 'subtitle-bad.txt',
     //     mimeType: 'application/octet-stream',
-    //     statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+    //     expectedStatus: HttpStatusCode.BAD_REQUEST_400
     //   })
     // })
 
     it('Should succeed with a valid captionfile extension and octet-stream mime type', async function () {
-      await createVideoCaption({
-        url: server.url,
-        accessToken: server.accessToken,
+      await server.captions.add({
         language: 'zh',
         videoId: video.uuid,
         fixture: 'subtitle-good.srt',
@@ -183,7 +173,7 @@ describe('Test video captions API validator', function () {
     //     token: server.accessToken,
     //     fields,
     //     attaches,
-    //     statusCodeExpected: HttpStatusCode.INTERNAL_SERVER_ERROR_500
+    //     expectedStatus: HttpStatusCode.INTERNAL_SERVER_ERROR_500
     //   })
     // })
 
@@ -196,7 +186,7 @@ describe('Test video captions API validator', function () {
         token: server.accessToken,
         fields,
         attaches,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -210,12 +200,12 @@ describe('Test video captions API validator', function () {
       await makeGetRequest({
         url: server.url,
         path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions',
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
     it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path: path + video.shortUUID + '/captions', statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, path: path + video.shortUUID + '/captions', expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -233,7 +223,7 @@ describe('Test video captions API validator', function () {
         url: server.url,
         path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/fr',
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -257,12 +247,12 @@ describe('Test video captions API validator', function () {
 
     it('Should fail without access token', async function () {
       const captionPath = path + video.shortUUID + '/captions/fr'
-      await makeDeleteRequest({ url: server.url, path: captionPath, statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makeDeleteRequest({ url: server.url, path: captionPath, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a bad access token', async function () {
       const captionPath = path + video.shortUUID + '/captions/fr'
-      await makeDeleteRequest({ url: server.url, path: captionPath, token: 'coucou', statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makeDeleteRequest({ url: server.url, path: captionPath, token: 'coucou', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with another user', async function () {
@@ -271,7 +261,7 @@ describe('Test video captions API validator', function () {
         url: server.url,
         path: captionPath,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -281,7 +271,7 @@ describe('Test video captions API validator', function () {
         url: server.url,
         path: captionPath,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
index 5c02afd31d9fda8410ef4bad99bed4c1f8c9a708..2e63916d4334abce4829e6e19172fb7b4ae6711d 100644 (file)
@@ -3,43 +3,37 @@
 import 'mocha'
 import * as chai from 'chai'
 import { omit } from 'lodash'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   buildAbsoluteFixturePath,
+  ChannelsCommand,
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   cleanupTests,
-  createUser,
-  deleteVideoChannel,
-  flushAndRunServer,
-  getAccountVideoChannelsList,
-  immutableAssign,
+  createSingleServer,
   makeGetRequest,
   makePostBodyRequest,
   makePutBodyRequest,
   makeUploadRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { VideoChannelUpdate } from '../../../../shared/models/videos'
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoChannelUpdate } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test video channels API validator', function () {
   const videoChannelPath = '/api/v1/video-channels'
-  let server: ServerInfo
+  let server: PeerTubeServer
   let accessTokenUser: string
+  let command: ChannelsCommand
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
@@ -49,9 +43,11 @@ describe('Test video channels API validator', function () {
     }
 
     {
-      await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-      accessTokenUser = await userLogin(server, user)
+      await server.users.create({ username: user.username, password: user.password })
+      accessTokenUser = await server.login.getAccessToken(user)
     }
+
+    command = server.channels
   })
 
   describe('When listing a video channels', function () {
@@ -84,14 +80,14 @@ describe('Test video channels API validator', function () {
     })
 
     it('Should fail with a unknown account', async function () {
-      await getAccountVideoChannelsList({ url: server.url, accountName: 'unknown', specialStatus: HttpStatusCode.NOT_FOUND_404 })
+      await server.channels.listByAccount({ accountName: 'unknown', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should succeed with the correct parameters', async function () {
       await makeGetRequest({
         url: server.url,
         path: accountChannelPath,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -110,7 +106,7 @@ describe('Test video channels API validator', function () {
         path: videoChannelPath,
         token: 'none',
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -125,7 +121,7 @@ describe('Test video channels API validator', function () {
     })
 
     it('Should fail with a bad name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { name: 'super name' })
+      const fields = { ...baseCorrectParams, name: 'super name' }
       await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
     })
 
@@ -135,17 +131,17 @@ describe('Test video channels API validator', function () {
     })
 
     it('Should fail with a long name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { displayName: 'super'.repeat(25) })
+      const fields = { ...baseCorrectParams, displayName: 'super'.repeat(25) }
       await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
     })
 
     it('Should fail with a long description', async function () {
-      const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(201) })
+      const fields = { ...baseCorrectParams, description: 'super'.repeat(201) }
       await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
     })
 
     it('Should fail with a long support text', async function () {
-      const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
+      const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
       await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
     })
 
@@ -155,7 +151,7 @@ describe('Test video channels API validator', function () {
         path: videoChannelPath,
         token: server.accessToken,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
 
@@ -165,7 +161,7 @@ describe('Test video channels API validator', function () {
         path: videoChannelPath,
         token: server.accessToken,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
   })
@@ -189,7 +185,7 @@ describe('Test video channels API validator', function () {
         path,
         token: 'hi',
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -199,27 +195,27 @@ describe('Test video channels API validator', function () {
         path,
         token: accessTokenUser,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
     it('Should fail with a long name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { displayName: 'super'.repeat(25) })
+      const fields = { ...baseCorrectParams, displayName: 'super'.repeat(25) }
       await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a long description', async function () {
-      const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(201) })
+      const fields = { ...baseCorrectParams, description: 'super'.repeat(201) }
       await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a long support text', async function () {
-      const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
+      const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
       await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad bulkVideosSupportUpdate field', async function () {
-      const fields = immutableAssign(baseCorrectParams, { bulkVideosSupportUpdate: 'super' })
+      const fields = { ...baseCorrectParams, bulkVideosSupportUpdate: 'super' }
       await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
@@ -229,7 +225,7 @@ describe('Test video channels API validator', function () {
         path,
         token: server.accessToken,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -274,7 +270,7 @@ describe('Test video channels API validator', function () {
           path: `${path}/${type}/pick`,
           fields,
           attaches,
-          statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401
         })
       }
     })
@@ -291,7 +287,7 @@ describe('Test video channels API validator', function () {
           token: server.accessToken,
           fields,
           attaches,
-          statusCodeExpected: HttpStatusCode.OK_200
+          expectedStatus: HttpStatusCode.OK_200
         })
       }
     })
@@ -302,7 +298,7 @@ describe('Test video channels API validator', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: videoChannelPath,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body.data).to.be.an('array')
@@ -312,7 +308,7 @@ describe('Test video channels API validator', function () {
       await makeGetRequest({
         url: server.url,
         path: videoChannelPath + '/super_channel2',
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -320,30 +316,30 @@ describe('Test video channels API validator', function () {
       await makeGetRequest({
         url: server.url,
         path: videoChannelPath + '/super_channel',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
 
   describe('When deleting a video channel', function () {
     it('Should fail with a non authenticated user', async function () {
-      await deleteVideoChannel(server.url, 'coucou', 'super_channel', HttpStatusCode.UNAUTHORIZED_401)
+      await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with another authenticated user', async function () {
-      await deleteVideoChannel(server.url, accessTokenUser, 'super_channel', HttpStatusCode.FORBIDDEN_403)
+      await command.delete({ token: accessTokenUser, channelName: 'super_channel', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with an unknown video channel id', async function () {
-      await deleteVideoChannel(server.url, server.accessToken, 'super_channel2', HttpStatusCode.NOT_FOUND_404)
+      await command.delete({ channelName: 'super_channel2', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should succeed with the correct parameters', async function () {
-      await deleteVideoChannel(server.url, server.accessToken, 'super_channel')
+      await command.delete({ channelName: 'super_channel' })
     })
 
     it('Should fail to delete the last user video channel', async function () {
-      await deleteVideoChannel(server.url, server.accessToken, 'root_channel', HttpStatusCode.CONFLICT_409)
+      await command.delete({ channelName: 'root_channel', expectedStatus: HttpStatusCode.CONFLICT_409 })
     })
   })
 
index a38420851f8d109b394898a7ad9539755b69a1c4..2d9ee1e0d2fa50c43ebd09a333a94148bd38fcee 100644 (file)
@@ -2,33 +2,26 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { VideoCreateResult } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   cleanupTests,
-  createUser,
-  flushAndRunServer,
+  createSingleServer,
   makeDeleteRequest,
   makeGetRequest,
   makePostBodyRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo,
-  userLogin
-} from '../../../../shared/extra-utils'
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoCreateResult } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test video comments API validator', function () {
   let pathThread: string
   let pathComment: string
-  let server: ServerInfo
+  let server: PeerTubeServer
   let video: VideoCreateResult
   let userAccessToken: string
   let userAccessToken2: string
@@ -39,32 +32,31 @@ describe('Test video comments API validator', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
     {
-      const res = await uploadVideo(server.url, server.accessToken, {})
-      video = res.body.video
+      video = await server.videos.upload({ attributes: {} })
       pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads'
     }
 
     {
-      const res = await addVideoCommentThread(server.url, server.accessToken, video.uuid, 'coucou')
-      commentId = res.body.comment.id
+      const created = await server.comments.createThread({ videoId: video.uuid, text: 'coucou' })
+      commentId = created.id
       pathComment = '/api/v1/videos/' + video.uuid + '/comments/' + commentId
     }
 
     {
       const user = { username: 'user1', password: 'my super password' }
-      await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-      userAccessToken = await userLogin(server, user)
+      await server.users.create({ username: user.username, password: user.password })
+      userAccessToken = await server.login.getAccessToken(user)
     }
 
     {
       const user = { username: 'user2', password: 'my super password' }
-      await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-      userAccessToken2 = await userLogin(server, user)
+      await server.users.create({ username: user.username, password: user.password })
+      userAccessToken2 = await server.login.getAccessToken(user)
     }
   })
 
@@ -85,7 +77,7 @@ describe('Test video comments API validator', function () {
       await makeGetRequest({
         url: server.url,
         path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads',
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
   })
@@ -95,7 +87,7 @@ describe('Test video comments API validator', function () {
       await makeGetRequest({
         url: server.url,
         path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads/' + commentId,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -103,7 +95,7 @@ describe('Test video comments API validator', function () {
       await makeGetRequest({
         url: server.url,
         path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/156',
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -111,7 +103,7 @@ describe('Test video comments API validator', function () {
       await makeGetRequest({
         url: server.url,
         path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/' + commentId,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -127,7 +119,7 @@ describe('Test video comments API validator', function () {
         path: pathThread,
         token: 'none',
         fields,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -160,7 +152,7 @@ describe('Test video comments API validator', function () {
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -173,7 +165,7 @@ describe('Test video comments API validator', function () {
         path: pathThread,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
@@ -188,7 +180,7 @@ describe('Test video comments API validator', function () {
         path: pathComment,
         token: 'none',
         fields,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -221,7 +213,7 @@ describe('Test video comments API validator', function () {
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -235,7 +227,7 @@ describe('Test video comments API validator', function () {
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -248,14 +240,14 @@ describe('Test video comments API validator', function () {
         path: pathComment,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
 
   describe('When removing video comments', function () {
     it('Should fail with a non authenticated user', async function () {
-      await makeDeleteRequest({ url: server.url, path: pathComment, token: 'none', statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makeDeleteRequest({ url: server.url, path: pathComment, token: 'none', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with another user', async function () {
@@ -263,32 +255,32 @@ describe('Test video comments API validator', function () {
         url: server.url,
         path: pathComment,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
     it('Should fail with an incorrect video', async function () {
       const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comments/' + commentId
-      await makeDeleteRequest({ url: server.url, path, token: server.accessToken, statusCodeExpected: HttpStatusCode.NOT_FOUND_404 })
+      await makeDeleteRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with an incorrect comment', async function () {
       const path = '/api/v1/videos/' + video.uuid + '/comments/124'
-      await makeDeleteRequest({ url: server.url, path, token: server.accessToken, statusCodeExpected: HttpStatusCode.NOT_FOUND_404 })
+      await makeDeleteRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should succeed with the same user', async function () {
       let commentToDelete: number
 
       {
-        const res = await addVideoCommentThread(server.url, userAccessToken, video.uuid, 'hello')
-        commentToDelete = res.body.comment.id
+        const created = await server.comments.createThread({ videoId: video.uuid, token: userAccessToken, text: 'hello' })
+        commentToDelete = created.id
       }
 
       const path = '/api/v1/videos/' + video.uuid + '/comments/' + commentToDelete
 
-      await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, statusCodeExpected: HttpStatusCode.FORBIDDEN_403 })
-      await makeDeleteRequest({ url: server.url, path, token: userAccessToken, statusCodeExpected: HttpStatusCode.NO_CONTENT_204 })
+      await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
+      await makeDeleteRequest({ url: server.url, path, token: userAccessToken, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
     })
 
     it('Should succeed with the owner of the video', async function () {
@@ -296,19 +288,19 @@ describe('Test video comments API validator', function () {
       let anotherVideoUUID: string
 
       {
-        const res = await uploadVideo(server.url, userAccessToken, { name: 'video' })
-        anotherVideoUUID = res.body.video.uuid
+        const { uuid } = await server.videos.upload({ token: userAccessToken, attributes: { name: 'video' } })
+        anotherVideoUUID = uuid
       }
 
       {
-        const res = await addVideoCommentThread(server.url, server.accessToken, anotherVideoUUID, 'hello')
-        commentToDelete = res.body.comment.id
+        const created = await server.comments.createThread({ videoId: anotherVideoUUID, text: 'hello' })
+        commentToDelete = created.id
       }
 
       const path = '/api/v1/videos/' + anotherVideoUUID + '/comments/' + commentToDelete
 
-      await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, statusCodeExpected: HttpStatusCode.FORBIDDEN_403 })
-      await makeDeleteRequest({ url: server.url, path, token: userAccessToken, statusCodeExpected: HttpStatusCode.NO_CONTENT_204 })
+      await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
+      await makeDeleteRequest({ url: server.url, path, token: userAccessToken, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
     })
 
     it('Should succeed with the correct parameters', async function () {
@@ -316,15 +308,14 @@ describe('Test video comments API validator', function () {
         url: server.url,
         path: pathComment,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
 
   describe('When a video has comments disabled', function () {
     before(async function () {
-      const res = await uploadVideo(server.url, server.accessToken, { commentsEnabled: false })
-      video = res.body.video
+      video = await server.videos.upload({ attributes: { commentsEnabled: false } })
       pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads'
     })
 
@@ -332,7 +323,7 @@ describe('Test video comments API validator', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: pathThread,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
       expect(res.body.total).to.equal(0)
       expect(res.body.data).to.have.lengthOf(0)
@@ -349,7 +340,7 @@ describe('Test video comments API validator', function () {
         path: pathThread,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
 
@@ -375,7 +366,7 @@ describe('Test video comments API validator', function () {
       await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -384,7 +375,7 @@ describe('Test video comments API validator', function () {
         url: server.url,
         path,
         token: userAccessToken,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
@@ -399,7 +390,7 @@ describe('Test video comments API validator', function () {
           searchAccount: 'toto',
           searchVideo: 'toto'
         },
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
index a27b624d0fde8ec09137d66758f2986658b39e1f..d6d745488c5c973bd8dac192157bb478383c8ada 100644 (file)
@@ -2,33 +2,25 @@
 
 import 'mocha'
 import { omit } from 'lodash'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   buildAbsoluteFixturePath,
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   cleanupTests,
-  createUser,
-  flushAndRunServer,
-  getMyUserInformation,
-  immutableAssign,
+  createSingleServer,
+  FIXTURE_URLS,
   makeGetRequest,
   makePostBodyRequest,
   makeUploadRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  updateCustomSubConfig,
-  userLogin
-} from '../../../../shared/extra-utils'
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { getGoodVideoUrl, getMagnetURI } from '../../../../shared/extra-utils/videos/video-imports'
-import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoPrivacy } from '@shared/models'
 
 describe('Test video imports API validator', function () {
   const path = '/api/v1/videos/imports'
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken = ''
   let channelId: number
 
@@ -37,18 +29,18 @@ describe('Test video imports API validator', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
     const username = 'user1'
     const password = 'my super password'
-    await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
-    userAccessToken = await userLogin(server, { username, password })
+    await server.users.create({ username: username, password: password })
+    userAccessToken = await server.login.getAccessToken({ username, password })
 
     {
-      const res = await getMyUserInformation(server.url, server.accessToken)
-      channelId = res.body.videoChannels[0].id
+      const { videoChannels } = await server.users.getMyInfo()
+      channelId = videoChannels[0].id
     }
   })
 
@@ -68,7 +60,7 @@ describe('Test video imports API validator', function () {
     })
 
     it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path: myPath, statusCodeExpected: HttpStatusCode.OK_200, token: server.accessToken })
+      await makeGetRequest({ url: server.url, path: myPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
     })
   })
 
@@ -77,7 +69,7 @@ describe('Test video imports API validator', function () {
 
     before(function () {
       baseCorrectParams = {
-        targetUrl: getGoodVideoUrl(),
+        targetUrl: FIXTURE_URLS.goodVideo,
         name: 'my super name',
         category: 5,
         licence: 1,
@@ -106,48 +98,48 @@ describe('Test video imports API validator', function () {
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
     it('Should fail with a bad target url', async function () {
-      const fields = immutableAssign(baseCorrectParams, { targetUrl: 'htt://hello' })
+      const fields = { ...baseCorrectParams, targetUrl: 'htt://hello' }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a long name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) })
+      const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad category', async function () {
-      const fields = immutableAssign(baseCorrectParams, { category: 125 })
+      const fields = { ...baseCorrectParams, category: 125 }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad licence', async function () {
-      const fields = immutableAssign(baseCorrectParams, { licence: 125 })
+      const fields = { ...baseCorrectParams, licence: 125 }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad language', async function () {
-      const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) })
+      const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a long description', async function () {
-      const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
+      const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a long support text', async function () {
-      const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
+      const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -159,7 +151,7 @@ describe('Test video imports API validator', function () {
     })
 
     it('Should fail with a bad channel', async function () {
-      const fields = immutableAssign(baseCorrectParams, { channelId: 545454 })
+      const fields = { ...baseCorrectParams, channelId: 545454 }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -169,31 +161,31 @@ describe('Test video imports API validator', function () {
         username: 'fake',
         password: 'fake_password'
       }
-      await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
+      await server.users.create({ username: user.username, password: user.password })
 
-      const accessTokenUser = await userLogin(server, user)
-      const res = await getMyUserInformation(server.url, accessTokenUser)
-      const customChannelId = res.body.videoChannels[0].id
+      const accessTokenUser = await server.login.getAccessToken(user)
+      const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser })
+      const customChannelId = videoChannels[0].id
 
-      const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId })
+      const fields = { ...baseCorrectParams, channelId: customChannelId }
 
       await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
     })
 
     it('Should fail with too many tags', async function () {
-      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] })
+      const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a tag length too low', async function () {
-      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] })
+      const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
     it('Should fail with a tag length too big', async function () {
-      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] })
+      const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -245,7 +237,7 @@ describe('Test video imports API validator', function () {
 
     it('Should fail with an invalid magnet URI', async function () {
       let fields = omit(baseCorrectParams, 'targetUrl')
-      fields = immutableAssign(fields, { magnetUri: 'blabla' })
+      fields = { ...fields, magnetUri: 'blabla' }
 
       await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
@@ -258,19 +250,21 @@ describe('Test video imports API validator', function () {
         path,
         token: server.accessToken,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
 
     it('Should forbid to import http videos', async function () {
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        import: {
-          videos: {
-            http: {
-              enabled: false
-            },
-            torrent: {
-              enabled: true
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          import: {
+            videos: {
+              http: {
+                enabled: false
+              },
+              torrent: {
+                enabled: true
+              }
             }
           }
         }
@@ -281,33 +275,35 @@ describe('Test video imports API validator', function () {
         path,
         token: server.accessToken,
         fields: baseCorrectParams,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
 
     it('Should forbid to import torrent videos', async function () {
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        import: {
-          videos: {
-            http: {
-              enabled: true
-            },
-            torrent: {
-              enabled: false
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          import: {
+            videos: {
+              http: {
+                enabled: true
+              },
+              torrent: {
+                enabled: false
+              }
             }
           }
         }
       })
 
       let fields = omit(baseCorrectParams, 'targetUrl')
-      fields = immutableAssign(fields, { magnetUri: getMagnetURI() })
+      fields = { ...fields, magnetUri: FIXTURE_URLS.magnet }
 
       await makePostBodyRequest({
         url: server.url,
         path,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
 
       fields = omit(fields, 'magnetUri')
@@ -321,7 +317,7 @@ describe('Test video imports API validator', function () {
         token: server.accessToken,
         fields,
         attaches,
-        statusCodeExpected: HttpStatusCode.CONFLICT_409
+        expectedStatus: HttpStatusCode.CONFLICT_409
       })
     })
   })
index 18253d11aef3e8deeffac14a179e7221e72a12df..e4d541b480f213ec043e01ac431ab9fbae430678 100644 (file)
@@ -1,34 +1,31 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { VideoPlaylistCreateResult, VideoPlaylistPrivacy, VideoPlaylistType } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
-  addVideoInPlaylist,
   checkBadCountPagination,
   checkBadSortPagination,
   checkBadStartPagination,
   cleanupTests,
-  createVideoPlaylist,
-  deleteVideoPlaylist,
-  flushAndRunServer,
-  generateUserAccessToken,
-  getAccountPlaylistsListWithToken,
-  getVideoPlaylist,
-  immutableAssign,
+  createSingleServer,
   makeGetRequest,
-  removeVideoFromPlaylist,
-  reorderVideosPlaylist,
-  ServerInfo,
+  PeerTubeServer,
+  PlaylistsCommand,
   setAccessTokensToServers,
-  setDefaultVideoChannel,
-  updateVideoPlaylist,
-  updateVideoPlaylistElement,
-  uploadVideoAndGetId
-} from '../../../../shared/extra-utils'
+  setDefaultVideoChannel
+} from '@shared/extra-utils'
+import {
+  HttpStatusCode,
+  VideoPlaylistCreate,
+  VideoPlaylistCreateResult,
+  VideoPlaylistElementCreate,
+  VideoPlaylistElementUpdate,
+  VideoPlaylistPrivacy,
+  VideoPlaylistReorder,
+  VideoPlaylistType
+} from '@shared/models'
 
 describe('Test video playlists API validator', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken: string
 
   let playlist: VideoPlaylistCreateResult
@@ -36,49 +33,54 @@ describe('Test video playlists API validator', function () {
 
   let watchLaterPlaylistId: number
   let videoId: number
-  let playlistElementId: number
+  let elementId: number
+
+  let command: PlaylistsCommand
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
     await setDefaultVideoChannel([ server ])
 
-    userAccessToken = await generateUserAccessToken(server, 'user1')
-    videoId = (await uploadVideoAndGetId({ server, videoName: 'video 1' })).id
+    userAccessToken = await server.users.generateUserAndToken('user1')
+    videoId = (await server.videos.quickUpload({ name: 'video 1' })).id
+
+    command = server.playlists
 
     {
-      const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER)
-      watchLaterPlaylistId = res.body.data[0].id
+      const { data } = await command.listByAccount({
+        token: server.accessToken,
+        handle: 'root',
+        start: 0,
+        count: 5,
+        playlistType: VideoPlaylistType.WATCH_LATER
+      })
+      watchLaterPlaylistId = data[0].id
     }
 
     {
-      const res = await createVideoPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
+      playlist = await command.create({
+        attributes: {
           displayName: 'super playlist',
           privacy: VideoPlaylistPrivacy.PUBLIC,
-          videoChannelId: server.videoChannel.id
+          videoChannelId: server.store.channel.id
         }
       })
-      playlist = res.body.videoPlaylist
     }
 
     {
-      const res = await createVideoPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
+      const created = await command.create({
+        attributes: {
           displayName: 'private',
           privacy: VideoPlaylistPrivacy.PRIVATE
         }
       })
-      privatePlaylistUUID = res.body.videoPlaylist.uuid
+      privatePlaylistUUID = created.uuid
     }
   })
 
@@ -117,7 +119,7 @@ describe('Test video playlists API validator', function () {
       await makeGetRequest({
         url: server.url,
         path: accountPath,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404,
+        expectedStatus: HttpStatusCode.NOT_FOUND_404,
         token: server.accessToken
       })
     })
@@ -128,18 +130,18 @@ describe('Test video playlists API validator', function () {
       await makeGetRequest({
         url: server.url,
         path: accountPath,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404,
+        expectedStatus: HttpStatusCode.NOT_FOUND_404,
         token: server.accessToken
       })
     })
 
     it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path: globalPath, statusCodeExpected: HttpStatusCode.OK_200, token: server.accessToken })
-      await makeGetRequest({ url: server.url, path: accountPath, statusCodeExpected: HttpStatusCode.OK_200, token: server.accessToken })
+      await makeGetRequest({ url: server.url, path: globalPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
+      await makeGetRequest({ url: server.url, path: accountPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
       await makeGetRequest({
         url: server.url,
         path: videoChannelPath,
-        statusCodeExpected: HttpStatusCode.OK_200,
+        expectedStatus: HttpStatusCode.OK_200,
         token: server.accessToken
       })
     })
@@ -157,141 +159,144 @@ describe('Test video playlists API validator', function () {
     })
 
     it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path: path + playlist.shortUUID + '/videos', statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, path: path + playlist.shortUUID + '/videos', expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
   describe('When getting a video playlist', function () {
     it('Should fail with a bad id or uuid', async function () {
-      await getVideoPlaylist(server.url, 'toto', HttpStatusCode.BAD_REQUEST_400)
+      await command.get({ playlistId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should fail with an unknown playlist', async function () {
-      await getVideoPlaylist(server.url, 42, HttpStatusCode.NOT_FOUND_404)
+      await command.get({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail to get an unlisted playlist with the number id', async function () {
-      const res = await createVideoPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
+      const playlist = await command.create({
+        attributes: {
           displayName: 'super playlist',
-          videoChannelId: server.videoChannel.id,
+          videoChannelId: server.store.channel.id,
           privacy: VideoPlaylistPrivacy.UNLISTED
         }
       })
-      const playlist = res.body.videoPlaylist
 
-      await getVideoPlaylist(server.url, playlist.id, HttpStatusCode.NOT_FOUND_404)
-      await getVideoPlaylist(server.url, playlist.uuid, HttpStatusCode.OK_200)
+      await command.get({ playlistId: playlist.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      await command.get({ playlistId: playlist.uuid, expectedStatus: HttpStatusCode.OK_200 })
     })
 
     it('Should succeed with the correct params', async function () {
-      await getVideoPlaylist(server.url, playlist.uuid, HttpStatusCode.OK_200)
+      await command.get({ playlistId: playlist.uuid, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
   describe('When creating/updating a video playlist', function () {
-    const getBase = (playlistAttrs: any = {}, wrapper: any = {}) => {
-      return Object.assign({
-        expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: Object.assign({
+    const getBase = (
+      attributes?: Partial<VideoPlaylistCreate>,
+      wrapper?: Partial<Parameters<PlaylistsCommand['create']>[0]>
+    ) => {
+      return {
+        attributes: {
           displayName: 'display name',
           privacy: VideoPlaylistPrivacy.UNLISTED,
           thumbnailfile: 'thumbnail.jpg',
-          videoChannelId: server.videoChannel.id
-        }, playlistAttrs)
-      }, wrapper)
+          videoChannelId: server.store.channel.id,
+
+          ...attributes
+        },
+
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400,
+
+        ...wrapper
+      }
     }
     const getUpdate = (params: any, playlistId: number | string) => {
-      return immutableAssign(params, { playlistId: playlistId })
+      return { ...params, playlistId: playlistId }
     }
 
     it('Should fail with an unauthenticated user', async function () {
       const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
 
-      await createVideoPlaylist(params)
-      await updateVideoPlaylist(getUpdate(params, playlist.shortUUID))
+      await command.create(params)
+      await command.update(getUpdate(params, playlist.shortUUID))
     })
 
     it('Should fail without displayName', async function () {
       const params = getBase({ displayName: undefined })
 
-      await createVideoPlaylist(params)
+      await command.create(params)
     })
 
     it('Should fail with an incorrect display name', async function () {
       const params = getBase({ displayName: 's'.repeat(300) })
 
-      await createVideoPlaylist(params)
-      await updateVideoPlaylist(getUpdate(params, playlist.shortUUID))
+      await command.create(params)
+      await command.update(getUpdate(params, playlist.shortUUID))
     })
 
     it('Should fail with an incorrect description', async function () {
       const params = getBase({ description: 't' })
 
-      await createVideoPlaylist(params)
-      await updateVideoPlaylist(getUpdate(params, playlist.shortUUID))
+      await command.create(params)
+      await command.update(getUpdate(params, playlist.shortUUID))
     })
 
     it('Should fail with an incorrect privacy', async function () {
       const params = getBase({ privacy: 45 })
 
-      await createVideoPlaylist(params)
-      await updateVideoPlaylist(getUpdate(params, playlist.shortUUID))
+      await command.create(params)
+      await command.update(getUpdate(params, playlist.shortUUID))
     })
 
     it('Should fail with an unknown video channel id', async function () {
       const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 })
 
-      await createVideoPlaylist(params)
-      await updateVideoPlaylist(getUpdate(params, playlist.shortUUID))
+      await command.create(params)
+      await command.update(getUpdate(params, playlist.shortUUID))
     })
 
     it('Should fail with an incorrect thumbnail file', async function () {
       const params = getBase({ thumbnailfile: 'video_short.mp4' })
 
-      await createVideoPlaylist(params)
-      await updateVideoPlaylist(getUpdate(params, playlist.shortUUID))
+      await command.create(params)
+      await command.update(getUpdate(params, playlist.shortUUID))
     })
 
     it('Should fail with a thumbnail file too big', async function () {
       const params = getBase({ thumbnailfile: 'preview-big.png' })
 
-      await createVideoPlaylist(params)
-      await updateVideoPlaylist(getUpdate(params, playlist.shortUUID))
+      await command.create(params)
+      await command.update(getUpdate(params, playlist.shortUUID))
     })
 
     it('Should fail to set "public" a playlist not assigned to a channel', async function () {
       const params = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: undefined })
-      const params2 = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: 'null' })
-      const params3 = getBase({ privacy: undefined, videoChannelId: 'null' })
+      const params2 = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: 'null' as any })
+      const params3 = getBase({ privacy: undefined, videoChannelId: 'null' as any })
 
-      await createVideoPlaylist(params)
-      await createVideoPlaylist(params2)
-      await updateVideoPlaylist(getUpdate(params, privatePlaylistUUID))
-      await updateVideoPlaylist(getUpdate(params2, playlist.shortUUID))
-      await updateVideoPlaylist(getUpdate(params3, playlist.shortUUID))
+      await command.create(params)
+      await command.create(params2)
+      await command.update(getUpdate(params, privatePlaylistUUID))
+      await command.update(getUpdate(params2, playlist.shortUUID))
+      await command.update(getUpdate(params3, playlist.shortUUID))
     })
 
     it('Should fail with an unknown playlist to update', async function () {
-      await updateVideoPlaylist(getUpdate(
+      await command.update(getUpdate(
         getBase({}, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }),
         42
       ))
     })
 
     it('Should fail to update a playlist of another user', async function () {
-      await updateVideoPlaylist(getUpdate(
+      await command.update(getUpdate(
         getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }),
         playlist.shortUUID
       ))
     })
 
     it('Should fail to update the watch later playlist', async function () {
-      await updateVideoPlaylist(getUpdate(
+      await command.update(getUpdate(
         getBase({}, { expectedStatus: HttpStatusCode.BAD_REQUEST_400 }),
         watchLaterPlaylistId
       ))
@@ -300,146 +305,158 @@ describe('Test video playlists API validator', function () {
     it('Should succeed with the correct params', async function () {
       {
         const params = getBase({}, { expectedStatus: HttpStatusCode.OK_200 })
-        await createVideoPlaylist(params)
+        await command.create(params)
       }
 
       {
         const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 })
-        await updateVideoPlaylist(getUpdate(params, playlist.shortUUID))
+        await command.update(getUpdate(params, playlist.shortUUID))
       }
     })
   })
 
   describe('When adding an element in a playlist', function () {
-    const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
-      return Object.assign({
-        expectedStatus: HttpStatusCode.BAD_REQUEST_400,
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlist.id,
-        elementAttrs: Object.assign({
+    const getBase = (
+      attributes?: Partial<VideoPlaylistElementCreate>,
+      wrapper?: Partial<Parameters<PlaylistsCommand['addElement']>[0]>
+    ) => {
+      return {
+        attributes: {
           videoId,
           startTimestamp: 2,
-          stopTimestamp: 3
-        }, elementAttrs)
-      }, wrapper)
+          stopTimestamp: 3,
+
+          ...attributes
+        },
+
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400,
+        playlistId: playlist.id,
+
+        ...wrapper
+      }
     }
 
     it('Should fail with an unauthenticated user', async function () {
       const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
-      await addVideoInPlaylist(params)
+      await command.addElement(params)
     })
 
     it('Should fail with the playlist of another user', async function () {
       const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
-      await addVideoInPlaylist(params)
+      await command.addElement(params)
     })
 
     it('Should fail with an unknown or incorrect playlist id', async function () {
       {
         const params = getBase({}, { playlistId: 'toto' })
-        await addVideoInPlaylist(params)
+        await command.addElement(params)
       }
 
       {
         const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
-        await addVideoInPlaylist(params)
+        await command.addElement(params)
       }
     })
 
     it('Should fail with an unknown or incorrect video id', async function () {
       const params = getBase({ videoId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 })
-      await addVideoInPlaylist(params)
+      await command.addElement(params)
     })
 
     it('Should fail with a bad start/stop timestamp', async function () {
       {
         const params = getBase({ startTimestamp: -42 })
-        await addVideoInPlaylist(params)
+        await command.addElement(params)
       }
 
       {
         const params = getBase({ stopTimestamp: 'toto' as any })
-        await addVideoInPlaylist(params)
+        await command.addElement(params)
       }
     })
 
     it('Succeed with the correct params', async function () {
       const params = getBase({}, { expectedStatus: HttpStatusCode.OK_200 })
-      const res = await addVideoInPlaylist(params)
-      playlistElementId = res.body.videoPlaylistElement.id
+      const created = await command.addElement(params)
+      elementId = created.id
     })
   })
 
   describe('When updating an element in a playlist', function () {
-    const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
-      return Object.assign({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: Object.assign({
+    const getBase = (
+      attributes?: Partial<VideoPlaylistElementUpdate>,
+      wrapper?: Partial<Parameters<PlaylistsCommand['updateElement']>[0]>
+    ) => {
+      return {
+        attributes: {
           startTimestamp: 1,
-          stopTimestamp: 2
-        }, elementAttrs),
-        playlistElementId,
+          stopTimestamp: 2,
+
+          ...attributes
+        },
+
+        elementId,
         playlistId: playlist.id,
-        expectedStatus: HttpStatusCode.BAD_REQUEST_400
-      }, wrapper)
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400,
+
+        ...wrapper
+      }
     }
 
     it('Should fail with an unauthenticated user', async function () {
       const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
-      await updateVideoPlaylistElement(params)
+      await command.updateElement(params)
     })
 
     it('Should fail with the playlist of another user', async function () {
       const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
-      await updateVideoPlaylistElement(params)
+      await command.updateElement(params)
     })
 
     it('Should fail with an unknown or incorrect playlist id', async function () {
       {
         const params = getBase({}, { playlistId: 'toto' })
-        await updateVideoPlaylistElement(params)
+        await command.updateElement(params)
       }
 
       {
         const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
-        await updateVideoPlaylistElement(params)
+        await command.updateElement(params)
       }
     })
 
     it('Should fail with an unknown or incorrect playlistElement id', async function () {
       {
-        const params = getBase({}, { playlistElementId: 'toto' })
-        await updateVideoPlaylistElement(params)
+        const params = getBase({}, { elementId: 'toto' })
+        await command.updateElement(params)
       }
 
       {
-        const params = getBase({}, { playlistElementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
-        await updateVideoPlaylistElement(params)
+        const params = getBase({}, { elementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+        await command.updateElement(params)
       }
     })
 
     it('Should fail with a bad start/stop timestamp', async function () {
       {
         const params = getBase({ startTimestamp: 'toto' as any })
-        await updateVideoPlaylistElement(params)
+        await command.updateElement(params)
       }
 
       {
         const params = getBase({ stopTimestamp: -42 })
-        await updateVideoPlaylistElement(params)
+        await command.updateElement(params)
       }
     })
 
     it('Should fail with an unknown element', async function () {
-      const params = getBase({}, { playlistElementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
-      await updateVideoPlaylistElement(params)
+      const params = getBase({}, { elementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      await command.updateElement(params)
     })
 
     it('Succeed with the correct params', async function () {
       const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 })
-      await updateVideoPlaylistElement(params)
+      await command.updateElement(params)
     })
   })
 
@@ -447,110 +464,111 @@ describe('Test video playlists API validator', function () {
     let videoId3: number
     let videoId4: number
 
-    const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
-      return Object.assign({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlist.shortUUID,
-        elementAttrs: Object.assign({
+    const getBase = (
+      attributes?: Partial<VideoPlaylistReorder>,
+      wrapper?: Partial<Parameters<PlaylistsCommand['reorderElements']>[0]>
+    ) => {
+      return {
+        attributes: {
           startPosition: 1,
           insertAfterPosition: 2,
-          reorderLength: 3
-        }, elementAttrs),
-        expectedStatus: HttpStatusCode.BAD_REQUEST_400
-      }, wrapper)
+          reorderLength: 3,
+
+          ...attributes
+        },
+
+        playlistId: playlist.shortUUID,
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400,
+
+        ...wrapper
+      }
     }
 
     before(async function () {
-      videoId3 = (await uploadVideoAndGetId({ server, videoName: 'video 3' })).id
-      videoId4 = (await uploadVideoAndGetId({ server, videoName: 'video 4' })).id
+      videoId3 = (await server.videos.quickUpload({ name: 'video 3' })).id
+      videoId4 = (await server.videos.quickUpload({ name: 'video 4' })).id
 
       for (const id of [ videoId3, videoId4 ]) {
-        await addVideoInPlaylist({
-          url: server.url,
-          token: server.accessToken,
-          playlistId: playlist.shortUUID,
-          elementAttrs: { videoId: id }
-        })
+        await command.addElement({ playlistId: playlist.shortUUID, attributes: { videoId: id } })
       }
     })
 
     it('Should fail with an unauthenticated user', async function () {
       const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
-      await reorderVideosPlaylist(params)
+      await command.reorderElements(params)
     })
 
     it('Should fail with the playlist of another user', async function () {
       const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
-      await reorderVideosPlaylist(params)
+      await command.reorderElements(params)
     })
 
     it('Should fail with an invalid playlist', async function () {
       {
         const params = getBase({}, { playlistId: 'toto' })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
 
       {
         const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
     })
 
     it('Should fail with an invalid start position', async function () {
       {
         const params = getBase({ startPosition: -1 })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
 
       {
         const params = getBase({ startPosition: 'toto' as any })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
 
       {
         const params = getBase({ startPosition: 42 })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
     })
 
     it('Should fail with an invalid insert after position', async function () {
       {
         const params = getBase({ insertAfterPosition: 'toto' as any })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
 
       {
         const params = getBase({ insertAfterPosition: -2 })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
 
       {
         const params = getBase({ insertAfterPosition: 42 })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
     })
 
     it('Should fail with an invalid reorder length', async function () {
       {
         const params = getBase({ reorderLength: 'toto' as any })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
 
       {
         const params = getBase({ reorderLength: -2 })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
 
       {
         const params = getBase({ reorderLength: 42 })
-        await reorderVideosPlaylist(params)
+        await command.reorderElements(params)
       }
     })
 
     it('Succeed with the correct params', async function () {
       const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 })
-      await reorderVideosPlaylist(params)
+      await command.reorderElements(params)
     })
   })
 
@@ -562,7 +580,7 @@ describe('Test video playlists API validator', function () {
         url: server.url,
         path,
         query: { videoIds: [ 1, 2 ] },
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       })
     })
 
@@ -595,82 +613,82 @@ describe('Test video playlists API validator', function () {
         token: server.accessToken,
         path,
         query: { videoIds: [ 1, 2 ] },
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     })
   })
 
   describe('When deleting an element in a playlist', function () {
-    const getBase = (wrapper: any = {}) => {
-      return Object.assign({
-        url: server.url,
-        token: server.accessToken,
-        playlistElementId,
+    const getBase = (wrapper: Partial<Parameters<PlaylistsCommand['removeElement']>[0]>) => {
+      return {
+        elementId,
         playlistId: playlist.uuid,
-        expectedStatus: HttpStatusCode.BAD_REQUEST_400
-      }, wrapper)
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400,
+
+        ...wrapper
+      }
     }
 
     it('Should fail with an unauthenticated user', async function () {
       const params = getBase({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
-      await removeVideoFromPlaylist(params)
+      await command.removeElement(params)
     })
 
     it('Should fail with the playlist of another user', async function () {
       const params = getBase({ token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
-      await removeVideoFromPlaylist(params)
+      await command.removeElement(params)
     })
 
     it('Should fail with an unknown or incorrect playlist id', async function () {
       {
         const params = getBase({ playlistId: 'toto' })
-        await removeVideoFromPlaylist(params)
+        await command.removeElement(params)
       }
 
       {
         const params = getBase({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
-        await removeVideoFromPlaylist(params)
+        await command.removeElement(params)
       }
     })
 
     it('Should fail with an unknown or incorrect video id', async function () {
       {
-        const params = getBase({ playlistElementId: 'toto' })
-        await removeVideoFromPlaylist(params)
+        const params = getBase({ elementId: 'toto' as any })
+        await command.removeElement(params)
       }
 
       {
-        const params = getBase({ playlistElementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
-        await removeVideoFromPlaylist(params)
+        const params = getBase({ elementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+        await command.removeElement(params)
       }
     })
 
     it('Should fail with an unknown element', async function () {
-      const params = getBase({ playlistElementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
-      await removeVideoFromPlaylist(params)
+      const params = getBase({ elementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      await command.removeElement(params)
     })
 
     it('Succeed with the correct params', async function () {
       const params = getBase({ expectedStatus: HttpStatusCode.NO_CONTENT_204 })
-      await removeVideoFromPlaylist(params)
+      await command.removeElement(params)
     })
   })
 
   describe('When deleting a playlist', function () {
     it('Should fail with an unknown playlist', async function () {
-      await deleteVideoPlaylist(server.url, server.accessToken, 42, HttpStatusCode.NOT_FOUND_404)
+      await command.delete({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with a playlist of another user', async function () {
-      await deleteVideoPlaylist(server.url, userAccessToken, playlist.uuid, HttpStatusCode.FORBIDDEN_403)
+      await command.delete({ token: userAccessToken, playlistId: playlist.uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with the watch later playlist', async function () {
-      await deleteVideoPlaylist(server.url, server.accessToken, watchLaterPlaylistId, HttpStatusCode.BAD_REQUEST_400)
+      await command.delete({ playlistId: watchLaterPlaylistId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should succeed with the correct params', async function () {
-      await deleteVideoPlaylist(server.url, server.accessToken, playlist.uuid)
+      await command.delete({ playlistId: playlist.uuid })
     })
   })
 
index 4d54a4fd04e757a6355cff7b20fb04f52b0f97d3..d08570bbea49caab47d670cd7dfd11d5f007a030 100644 (file)
@@ -3,18 +3,15 @@
 import 'mocha'
 import {
   cleanupTests,
-  createUser,
-  flushAndRunServer,
+  createSingleServer,
   makeGetRequest,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  setDefaultVideoChannel,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { UserRole } from '../../../../shared/models/users'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+  setDefaultVideoChannel
+} from '@shared/extra-utils'
+import { HttpStatusCode, UserRole } from '@shared/models'
 
-async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: HttpStatusCode) {
+async function testEndpoints (server: PeerTubeServer, token: string, filter: string, expectedStatus: HttpStatusCode) {
   const paths = [
     '/api/v1/video-channels/root_channel/videos',
     '/api/v1/accounts/root/videos',
@@ -30,13 +27,13 @@ async function testEndpoints (server: ServerInfo, token: string, filter: string,
       query: {
         filter
       },
-      statusCodeExpected
+      expectedStatus
     })
   }
 }
 
 describe('Test video filters validators', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken: string
   let moderatorAccessToken: string
 
@@ -45,28 +42,19 @@ describe('Test video filters validators', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
     await setDefaultVideoChannel([ server ])
 
     const user = { username: 'user1', password: 'my super password' }
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-    userAccessToken = await userLogin(server, user)
+    await server.users.create({ username: user.username, password: user.password })
+    userAccessToken = await server.login.getAccessToken(user)
 
     const moderator = { username: 'moderator', password: 'my super password' }
-    await createUser(
-      {
-        url: server.url,
-        accessToken: server.accessToken,
-        username: moderator.username,
-        password: moderator.password,
-        videoQuota: undefined,
-        videoQuotaDaily: undefined,
-        role: UserRole.MODERATOR
-      }
-    )
-    moderatorAccessToken = await userLogin(server, moderator)
+    await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
+
+    moderatorAccessToken = await server.login.getAccessToken(moderator)
   })
 
   describe('When setting a video filter', function () {
@@ -100,7 +88,7 @@ describe('Test video filters validators', function () {
         await makeGetRequest({
           url: server.url,
           path: '/feeds/videos.json',
-          statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401,
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
           query: {
             filter
           }
@@ -112,7 +100,7 @@ describe('Test video filters validators', function () {
       await makeGetRequest({
         url: server.url,
         path: '/feeds/videos.json',
-        statusCodeExpected: HttpStatusCode.OK_200,
+        expectedStatus: HttpStatusCode.OK_200,
         query: {
           filter: 'local'
         }
index 0e91fe0a8640ccd7b7e527705b7b7f3c0505babe..c3c309ed28663afe3eeef71d9c875c98e01c4458 100644 (file)
@@ -5,42 +5,39 @@ import {
   checkBadCountPagination,
   checkBadStartPagination,
   cleanupTests,
-  flushAndRunServer,
+  createSingleServer,
   makeGetRequest,
   makePostBodyRequest,
   makePutBodyRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo
-} from '../../../../shared/extra-utils'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test videos history API validator', function () {
   const myHistoryPath = '/api/v1/users/me/history/videos'
   const myHistoryRemove = myHistoryPath + '/remove'
   let watchingPath: string
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
-    const res = await uploadVideo(server.url, server.accessToken, {})
-    const videoUUID = res.body.video.uuid
-
-    watchingPath = '/api/v1/videos/' + videoUUID + '/watching'
+    const { uuid } = await server.videos.upload()
+    watchingPath = '/api/v1/videos/' + uuid + '/watching'
   })
 
   describe('When notifying a user is watching a video', function () {
 
     it('Should fail with an unauthenticated user', async function () {
       const fields = { currentTime: 5 }
-      await makePutBodyRequest({ url: server.url, path: watchingPath, fields, statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makePutBodyRequest({ url: server.url, path: watchingPath, fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with an incorrect video id', async function () {
@@ -51,7 +48,7 @@ describe('Test videos history API validator', function () {
         path,
         fields,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -64,7 +61,7 @@ describe('Test videos history API validator', function () {
         path,
         fields,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -75,7 +72,7 @@ describe('Test videos history API validator', function () {
         path: watchingPath,
         fields,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -87,7 +84,7 @@ describe('Test videos history API validator', function () {
         path: watchingPath,
         fields,
         token: server.accessToken,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -102,17 +99,17 @@ describe('Test videos history API validator', function () {
     })
 
     it('Should fail with an unauthenticated user', async function () {
-      await makeGetRequest({ url: server.url, path: myHistoryPath, statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makeGetRequest({ url: server.url, path: myHistoryPath, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should succeed with the correct params', async function () {
-      await makeGetRequest({ url: server.url, token: server.accessToken, path: myHistoryPath, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, token: server.accessToken, path: myHistoryPath, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
   describe('When removing user videos history', function () {
     it('Should fail with an unauthenticated user', async function () {
-      await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 })
+      await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should fail with a bad beforeDate parameter', async function () {
@@ -122,7 +119,7 @@ describe('Test videos history API validator', function () {
         token: server.accessToken,
         path: myHistoryRemove,
         fields: body,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -133,7 +130,7 @@ describe('Test videos history API validator', function () {
         token: server.accessToken,
         path: myHistoryRemove,
         fields: body,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
 
@@ -142,7 +139,7 @@ describe('Test videos history API validator', function () {
         url: server.url,
         token: server.accessToken,
         path: myHistoryRemove,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
index 69d7fc4713007950395880afe5ec8c1718716d27..c2139d74b3c9863157cdbb2d6e8c619c2c6a5be9 100644 (file)
@@ -1,29 +1,28 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../../shared/extra-utils'
-import { getVideosOverview } from '@shared/extra-utils/overviews/overviews'
+import { cleanupTests, createSingleServer, PeerTubeServer } from '@shared/extra-utils'
 
 describe('Test videos overview', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
   })
 
   describe('When getting videos overview', function () {
 
     it('Should fail with a bad pagination', async function () {
-      await getVideosOverview(server.url, 0, 400)
-      await getVideosOverview(server.url, 100, 400)
+      await server.overviews.getVideos({ page: 0, expectedStatus: 400 })
+      await server.overviews.getVideos({ page: 100, expectedStatus: 400 })
     })
 
     it('Should succeed with a good pagination', async function () {
-      await getVideosOverview(server.url, 1)
+      await server.overviews.getVideos({ page: 1 })
     })
   })
 
index 4d7a9a23bbd7ac6e22020dffb9dbe92fa3a561c2..e11ca0c828ef9960ec541cc5c9825a071bc5f445 100644 (file)
@@ -5,39 +5,28 @@ import * as chai from 'chai'
 import { omit } from 'lodash'
 import { join } from 'path'
 import { randomInt } from '@shared/core-utils'
-import { PeerTubeProblemDocument, VideoCreateResult } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
+  checkBadCountPagination,
+  checkBadSortPagination,
+  checkBadStartPagination,
   checkUploadVideoParam,
   cleanupTests,
-  createUser,
-  flushAndRunServer,
-  getMyUserInformation,
-  getVideo,
-  getVideosList,
-  immutableAssign,
+  createSingleServer,
   makeDeleteRequest,
   makeGetRequest,
   makePutBodyRequest,
   makeUploadRequest,
-  removeVideo,
+  PeerTubeServer,
   root,
-  ServerInfo,
-  setAccessTokensToServers,
-  userLogin
-} from '../../../../shared/extra-utils'
-import {
-  checkBadCountPagination,
-  checkBadSortPagination,
-  checkBadStartPagination
-} from '../../../../shared/extra-utils/requests/check-api-params'
-import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode, PeerTubeProblemDocument, VideoCreateResult, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test videos API validator', function () {
   const path = '/api/v1/videos/'
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken = ''
   let accountName: string
   let channelId: number
@@ -49,20 +38,20 @@ describe('Test videos API validator', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
     const username = 'user1'
     const password = 'my super password'
-    await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
-    userAccessToken = await userLogin(server, { username, password })
+    await server.users.create({ username: username, password: password })
+    userAccessToken = await server.login.getAccessToken({ username, password })
 
     {
-      const res = await getMyUserInformation(server.url, server.accessToken)
-      channelId = res.body.videoChannels[0].id
-      channelName = res.body.videoChannels[0].name
-      accountName = res.body.account.name + '@' + res.body.account.host
+      const body = await server.users.getMyInfo()
+      channelId = body.videoChannels[0].id
+      channelName = body.videoChannels[0].name
+      accountName = body.account.name + '@' + body.account.host
     }
   })
 
@@ -80,11 +69,11 @@ describe('Test videos API validator', function () {
     })
 
     it('Should fail with a bad skipVideos query', async function () {
-      await makeGetRequest({ url: server.url, path, statusCodeExpected: HttpStatusCode.OK_200, query: { skipCount: 'toto' } })
+      await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200, query: { skipCount: 'toto' } })
     })
 
     it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path, statusCodeExpected: HttpStatusCode.OK_200, query: { skipCount: false } })
+      await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200, query: { skipCount: false } })
     })
   })
 
@@ -94,7 +83,7 @@ describe('Test videos API validator', function () {
       await makeGetRequest({
         url: server.url,
         path: join(path, 'search'),
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
@@ -111,7 +100,7 @@ describe('Test videos API validator', function () {
     })
 
     it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -131,7 +120,7 @@ describe('Test videos API validator', function () {
     })
 
     it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, token: server.accessToken, path, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, token: server.accessToken, path, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -155,7 +144,7 @@ describe('Test videos API validator', function () {
     })
 
     it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -179,7 +168,7 @@ describe('Test videos API validator', function () {
     })
 
     it('Should success with the correct parameters', async function () {
-      await makeGetRequest({ url: server.url, path, statusCodeExpected: HttpStatusCode.OK_200 })
+      await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -214,70 +203,70 @@ describe('Test videos API validator', function () {
       it('Should fail with nothing', async function () {
         const fields = {}
         const attaches = {}
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail without name', async function () {
         const fields = omit(baseCorrectParams, 'name')
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a long name', async function () {
-        const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) })
+        const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a bad category', async function () {
-        const fields = immutableAssign(baseCorrectParams, { category: 125 })
+        const fields = { ...baseCorrectParams, category: 125 }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a bad licence', async function () {
-        const fields = immutableAssign(baseCorrectParams, { licence: 125 })
+        const fields = { ...baseCorrectParams, licence: 125 }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a bad language', async function () {
-        const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) })
+        const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a long description', async function () {
-        const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
+        const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a long support text', async function () {
-        const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
+        const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail without a channel', async function () {
         const fields = omit(baseCorrectParams, 'channelId')
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a bad channel', async function () {
-        const fields = immutableAssign(baseCorrectParams, { channelId: 545454 })
+        const fields = { ...baseCorrectParams, channelId: 545454 }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with another user channel', async function () {
@@ -285,69 +274,71 @@ describe('Test videos API validator', function () {
           username: 'fake' + randomInt(0, 1500),
           password: 'fake_password'
         }
-        await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
+        await server.users.create({ username: user.username, password: user.password })
 
-        const accessTokenUser = await userLogin(server, user)
-        const res = await getMyUserInformation(server.url, accessTokenUser)
-        const customChannelId = res.body.videoChannels[0].id
+        const accessTokenUser = await server.login.getAccessToken(user)
+        const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser })
+        const customChannelId = videoChannels[0].id
 
-        const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId })
+        const fields = { ...baseCorrectParams, channelId: customChannelId }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, userAccessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, userAccessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with too many tags', async function () {
-        const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] })
+        const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a tag length too low', async function () {
-        const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] })
+        const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a tag length too big', async function () {
-        const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] })
+        const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a bad schedule update (miss updateAt)', async function () {
-        const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } })
+        const fields = { ...baseCorrectParams, scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a bad schedule update (wrong updateAt)', async function () {
-        const fields = immutableAssign(baseCorrectParams, {
+        const fields = {
+          ...baseCorrectParams,
+
           scheduleUpdate: {
             privacy: VideoPrivacy.PUBLIC,
             updateAt: 'toto'
           }
-        })
+        }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a bad originally published at attribute', async function () {
-        const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' })
+        const fields = { ...baseCorrectParams, originallyPublishedAt: 'toto' }
         const attaches = baseCorrectAttaches
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail without an input file', async function () {
         const fields = baseCorrectParams
         const attaches = {}
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with an incorrect input file', async function () {
@@ -355,7 +346,7 @@ describe('Test videos API validator', function () {
         let attaches = { fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short_fake.webm') }
 
         await checkUploadVideoParam(
-          server.url,
+          server,
           server.accessToken,
           { ...fields, ...attaches },
           HttpStatusCode.UNPROCESSABLE_ENTITY_422,
@@ -364,7 +355,7 @@ describe('Test videos API validator', function () {
 
         attaches = { fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mkv') }
         await checkUploadVideoParam(
-          server.url,
+          server,
           server.accessToken,
           { ...fields, ...attaches },
           HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
@@ -379,7 +370,7 @@ describe('Test videos API validator', function () {
           fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
         }
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a big thumbnail file', async function () {
@@ -389,7 +380,7 @@ describe('Test videos API validator', function () {
           fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
         }
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with an incorrect preview file', async function () {
@@ -399,7 +390,7 @@ describe('Test videos API validator', function () {
           fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
         }
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should fail with a big preview file', async function () {
@@ -409,17 +400,17 @@ describe('Test videos API validator', function () {
           fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
         }
 
-        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
+        await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
       })
 
       it('Should report the appropriate error', async function () {
-        const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) })
+        const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
         const attaches = baseCorrectAttaches
 
         const attributes = { ...fields, ...attaches }
-        const res = await checkUploadVideoParam(server.url, server.accessToken, attributes, HttpStatusCode.BAD_REQUEST_400, mode)
+        const body = await checkUploadVideoParam(server, server.accessToken, attributes, HttpStatusCode.BAD_REQUEST_400, mode)
 
-        const error = res.body as PeerTubeProblemDocument
+        const error = body as unknown as PeerTubeProblemDocument
 
         if (mode === 'legacy') {
           expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy')
@@ -444,23 +435,27 @@ describe('Test videos API validator', function () {
 
         {
           const attaches = baseCorrectAttaches
-          await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode)
+          await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode)
         }
 
         {
-          const attaches = immutableAssign(baseCorrectAttaches, {
+          const attaches = {
+            ...baseCorrectAttaches,
+
             videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
-          })
+          }
 
-          await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode)
+          await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode)
         }
 
         {
-          const attaches = immutableAssign(baseCorrectAttaches, {
+          const attaches = {
+            ...baseCorrectAttaches,
+
             videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.ogv')
-          })
+          }
 
-          await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode)
+          await checkUploadVideoParam(server, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode)
         }
       })
     }
@@ -489,8 +484,8 @@ describe('Test videos API validator', function () {
     }
 
     before(async function () {
-      const res = await getVideosList(server.url)
-      video = res.body.data[0]
+      const { data } = await server.videos.list()
+      video = data[0]
     })
 
     it('Should fail with nothing', async function () {
@@ -511,84 +506,84 @@ describe('Test videos API validator', function () {
         path: path + '4da6fde3-88f7-4d16-b119-108df5630b06',
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
     it('Should fail with a long name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) })
+      const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad category', async function () {
-      const fields = immutableAssign(baseCorrectParams, { category: 125 })
+      const fields = { ...baseCorrectParams, category: 125 }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad licence', async function () {
-      const fields = immutableAssign(baseCorrectParams, { licence: 125 })
+      const fields = { ...baseCorrectParams, licence: 125 }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad language', async function () {
-      const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) })
+      const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a long description', async function () {
-      const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
+      const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a long support text', async function () {
-      const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
+      const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad channel', async function () {
-      const fields = immutableAssign(baseCorrectParams, { channelId: 545454 })
+      const fields = { ...baseCorrectParams, channelId: 545454 }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with too many tags', async function () {
-      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] })
+      const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a tag length too low', async function () {
-      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] })
+      const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a tag length too big', async function () {
-      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] })
+      const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad schedule update (miss updateAt)', async function () {
-      const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } })
+      const fields = { ...baseCorrectParams, scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad schedule update (wrong updateAt)', async function () {
-      const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } })
+      const fields = { ...baseCorrectParams, scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
 
     it('Should fail with a bad originally published at param', async function () {
-      const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' })
+      const fields = { ...baseCorrectParams, originallyPublishedAt: 'toto' }
 
       await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
     })
@@ -665,14 +660,14 @@ describe('Test videos API validator', function () {
         path: path + video.shortUUID,
         token: userAccessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
       })
     })
 
     it('Should fail with a video of another server')
 
     it('Shoud report the appropriate error', async function () {
-      const fields = immutableAssign(baseCorrectParams, { licence: 125 })
+      const fields = { ...baseCorrectParams, licence: 125 }
 
       const res = await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
       const error = res.body as PeerTubeProblemDocument
@@ -697,7 +692,7 @@ describe('Test videos API validator', function () {
         path: path + video.shortUUID,
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -707,7 +702,7 @@ describe('Test videos API validator', function () {
       const res = await makeGetRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body.data).to.be.an('array')
@@ -715,16 +710,16 @@ describe('Test videos API validator', function () {
     })
 
     it('Should fail without a correct uuid', async function () {
-      await getVideo(server.url, 'coucou', HttpStatusCode.BAD_REQUEST_400)
+      await server.videos.get({ id: 'coucou', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should return 404 with an incorrect video', async function () {
-      await getVideo(server.url, '4da6fde3-88f7-4d16-b119-108df5630b06', HttpStatusCode.NOT_FOUND_404)
+      await server.videos.get({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Shoud report the appropriate error', async function () {
-      const res = await getVideo(server.url, 'hi', HttpStatusCode.BAD_REQUEST_400)
-      const error = res.body as PeerTubeProblemDocument
+      const body = await server.videos.get({ id: 'hi', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      const error = body as unknown as PeerTubeProblemDocument
 
       expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo')
 
@@ -739,16 +734,16 @@ describe('Test videos API validator', function () {
     })
 
     it('Should succeed with the correct parameters', async function () {
-      await getVideo(server.url, video.shortUUID)
+      await server.videos.get({ id: video.shortUUID })
     })
   })
 
   describe('When rating a video', function () {
-    let videoId
+    let videoId: number
 
     before(async function () {
-      const res = await getVideosList(server.url)
-      videoId = res.body.data[0].id
+      const { data } = await server.videos.list()
+      videoId = data[0].id
     })
 
     it('Should fail without a valid uuid', async function () {
@@ -767,7 +762,7 @@ describe('Test videos API validator', function () {
         path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/rate',
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -787,7 +782,7 @@ describe('Test videos API validator', function () {
         path: path + videoId + '/rate',
         token: server.accessToken,
         fields,
-        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
       })
     })
   })
@@ -797,27 +792,27 @@ describe('Test videos API validator', function () {
       await makeDeleteRequest({
         url: server.url,
         path,
-        statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
       })
     })
 
     it('Should fail without a correct uuid', async function () {
-      await removeVideo(server.url, server.accessToken, 'hello', HttpStatusCode.BAD_REQUEST_400)
+      await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should fail with a video which does not exist', async function () {
-      await removeVideo(server.url, server.accessToken, '4da6fde3-88f7-4d16-b119-108df5630b06', HttpStatusCode.NOT_FOUND_404)
+      await server.videos.remove({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should fail with a video of another user without the appropriate right', async function () {
-      await removeVideo(server.url, userAccessToken, video.uuid, HttpStatusCode.FORBIDDEN_403)
+      await server.videos.remove({ token: userAccessToken, id: video.uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with a video of another server')
 
     it('Shoud report the appropriate error', async function () {
-      const res = await removeVideo(server.url, server.accessToken, 'hello', HttpStatusCode.BAD_REQUEST_400)
-      const error = res.body as PeerTubeProblemDocument
+      const body = await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      const error = body as PeerTubeProblemDocument
 
       expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo')
 
@@ -832,7 +827,7 @@ describe('Test videos API validator', function () {
     })
 
     it('Should succeed with the correct parameters', async function () {
-      await removeVideo(server.url, server.accessToken, video.uuid)
+      await server.videos.remove({ id: video.uuid })
     })
   })
 
index cc635de339d2102858ffbcfc08cf225187e98d97..4acde3cc55af033a1dbcfb8c1cc9dc6b0f6545c0 100644 (file)
@@ -2,31 +2,24 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { VideoDetails, VideoPrivacy } from '@shared/models'
+import { VideoPrivacy } from '@shared/models'
 import {
-  checkLiveCleanup,
+  checkLiveCleanupAfterSave,
   cleanupTests,
-  createLive,
+  ConfigCommand,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  generateUser,
-  getCustomConfigResolutions,
-  getVideo,
-  runAndTestFfmpegStreamError,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
   setDefaultVideoChannel,
-  updateCustomSubConfig,
-  updateUser,
   wait,
-  waitJobs,
-  waitUntilLivePublished
+  waitJobs
 } from '../../../../shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test live constraints', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let userId: number
   let userAccessToken: string
   let userChannelId: number
@@ -39,32 +32,28 @@ describe('Test live constraints', function () {
       saveReplay
     }
 
-    const res = await createLive(servers[0].url, userAccessToken, liveAttributes)
-    return res.body.video.uuid as string
+    const { uuid } = await servers[0].live.create({ token: userAccessToken, fields: liveAttributes })
+    return uuid
   }
 
   async function checkSaveReplay (videoId: string, resolutions = [ 720 ]) {
     for (const server of servers) {
-      const res = await getVideo(server.url, videoId)
-
-      const video: VideoDetails = res.body
+      const video = await server.videos.get({ id: videoId })
       expect(video.isLive).to.be.false
       expect(video.duration).to.be.greaterThan(0)
     }
 
-    await checkLiveCleanup(servers[0], videoId, resolutions)
+    await checkLiveCleanupAfterSave(servers[0], videoId, resolutions)
   }
 
   async function waitUntilLivePublishedOnAllServers (videoId: string) {
     for (const server of servers) {
-      await waitUntilLivePublished(server.url, server.accessToken, videoId)
+      await server.live.waitUntilPublished({ videoId })
     }
   }
 
   function updateQuota (options: { total: number, daily: number }) {
-    return updateUser({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
+    return servers[0].users.update({
       userId,
       videoQuota: options.total,
       videoQuotaDaily: options.daily
@@ -74,24 +63,26 @@ describe('Test live constraints', function () {
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      live: {
-        enabled: true,
-        allowReplay: true,
-        transcoding: {
-          enabled: false
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        live: {
+          enabled: true,
+          allowReplay: true,
+          transcoding: {
+            enabled: false
+          }
         }
       }
     })
 
     {
-      const res = await generateUser(servers[0], 'user1')
+      const res = await servers[0].users.generate('user1')
       userId = res.userId
       userChannelId = res.userChannelId
       userAccessToken = res.token
@@ -107,7 +98,7 @@ describe('Test live constraints', function () {
     this.timeout(60000)
 
     const userVideoLiveoId = await createLiveWrapper(false)
-    await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, false)
+    await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false })
   })
 
   it('Should have size limit depending on user global quota if save replay is enabled', async function () {
@@ -117,7 +108,7 @@ describe('Test live constraints', function () {
     await wait(5000)
 
     const userVideoLiveoId = await createLiveWrapper(true)
-    await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true)
+    await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
 
     await waitUntilLivePublishedOnAllServers(userVideoLiveoId)
     await waitJobs(servers)
@@ -134,7 +125,7 @@ describe('Test live constraints', function () {
     await updateQuota({ total: -1, daily: 1 })
 
     const userVideoLiveoId = await createLiveWrapper(true)
-    await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true)
+    await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
 
     await waitUntilLivePublishedOnAllServers(userVideoLiveoId)
     await waitJobs(servers)
@@ -151,26 +142,28 @@ describe('Test live constraints', function () {
     await updateQuota({ total: 10 * 1000 * 1000, daily: -1 })
 
     const userVideoLiveoId = await createLiveWrapper(true)
-    await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, false)
+    await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false })
   })
 
   it('Should have max duration limit', async function () {
     this.timeout(60000)
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      live: {
-        enabled: true,
-        allowReplay: true,
-        maxDuration: 1,
-        transcoding: {
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        live: {
           enabled: true,
-          resolutions: getCustomConfigResolutions(true)
+          allowReplay: true,
+          maxDuration: 1,
+          transcoding: {
+            enabled: true,
+            resolutions: ConfigCommand.getCustomConfigResolutions(true)
+          }
         }
       }
     })
 
     const userVideoLiveoId = await createLiveWrapper(true)
-    await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true)
+    await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
 
     await waitUntilLivePublishedOnAllServers(userVideoLiveoId)
     await waitJobs(servers)
index 71b7d28a885c23232943468e8a017d7ecb2bced1..f07d4cfecc7936b0df884abf4bd3da323ae45bdf 100644 (file)
@@ -2,59 +2,50 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { LiveVideoCreate, VideoDetails, VideoPrivacy, VideoState } from '@shared/models'
+import { LiveVideoCreate, VideoPrivacy, VideoState } from '@shared/models'
 import {
   cleanupTests,
-  createLive,
+  ConfigCommand,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getCustomConfigResolutions,
-  getLive,
-  getPlaylistsCount,
-  getVideo,
-  sendRTMPStreamInVideo,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
   setDefaultVideoChannel,
   stopFfmpeg,
-  updateCustomSubConfig,
-  updateLive,
   wait,
-  waitJobs,
-  waitUntilLivePublished,
-  waitUntilLiveWaiting
+  waitJobs
 } from '../../../../shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Permanent live', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let videoUUID: string
 
   async function createLiveWrapper (permanentLive: boolean) {
     const attributes: LiveVideoCreate = {
-      channelId: servers[0].videoChannel.id,
+      channelId: servers[0].store.channel.id,
       privacy: VideoPrivacy.PUBLIC,
       name: 'my super live',
       saveReplay: false,
       permanentLive
     }
 
-    const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
-    return res.body.video.uuid
+    const { uuid } = await servers[0].live.create({ fields: attributes })
+    return uuid
   }
 
   async function checkVideoState (videoId: string, state: VideoState) {
     for (const server of servers) {
-      const res = await getVideo(server.url, videoId)
-      expect((res.body as VideoDetails).state.id).to.equal(state)
+      const video = await server.videos.get({ id: videoId })
+      expect(video.state.id).to.equal(state)
     }
   }
 
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -63,14 +54,16 @@ describe('Permanent live', function () {
     // Server 1 and server 2 follow each other
     await doubleFollow(servers[0], servers[1])
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      live: {
-        enabled: true,
-        allowReplay: true,
-        maxDuration: -1,
-        transcoding: {
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        live: {
           enabled: true,
-          resolutions: getCustomConfigResolutions(true)
+          allowReplay: true,
+          maxDuration: -1,
+          transcoding: {
+            enabled: true,
+            resolutions: ConfigCommand.getCustomConfigResolutions(true)
+          }
         }
       }
     })
@@ -82,15 +75,15 @@ describe('Permanent live', function () {
     const videoUUID = await createLiveWrapper(false)
 
     {
-      const res = await getLive(servers[0].url, servers[0].accessToken, videoUUID)
-      expect(res.body.permanentLive).to.be.false
+      const live = await servers[0].live.get({ videoId: videoUUID })
+      expect(live.permanentLive).to.be.false
     }
 
-    await updateLive(servers[0].url, servers[0].accessToken, videoUUID, { permanentLive: true })
+    await servers[0].live.update({ videoId: videoUUID, fields: { permanentLive: true } })
 
     {
-      const res = await getLive(servers[0].url, servers[0].accessToken, videoUUID)
-      expect(res.body.permanentLive).to.be.true
+      const live = await servers[0].live.get({ videoId: videoUUID })
+      expect(live.permanentLive).to.be.true
     }
   })
 
@@ -99,8 +92,8 @@ describe('Permanent live', function () {
 
     videoUUID = await createLiveWrapper(true)
 
-    const res = await getLive(servers[0].url, servers[0].accessToken, videoUUID)
-    expect(res.body.permanentLive).to.be.true
+    const live = await servers[0].live.get({ videoId: videoUUID })
+    expect(live.permanentLive).to.be.true
 
     await waitJobs(servers)
   })
@@ -108,16 +101,16 @@ describe('Permanent live', function () {
   it('Should stream into this permanent live', async function () {
     this.timeout(120000)
 
-    const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, videoUUID)
+    const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID })
 
     for (const server of servers) {
-      await waitUntilLivePublished(server.url, server.accessToken, videoUUID)
+      await server.live.waitUntilPublished({ videoId: videoUUID })
     }
 
     await checkVideoState(videoUUID, VideoState.PUBLISHED)
 
-    await stopFfmpeg(command)
-    await waitUntilLiveWaiting(servers[0].url, servers[0].accessToken, videoUUID)
+    await stopFfmpeg(ffmpegCommand)
+    await servers[0].live.waitUntilWaiting({ videoId: videoUUID })
 
     await waitJobs(servers)
   })
@@ -129,9 +122,7 @@ describe('Permanent live', function () {
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideo(server.url, videoUUID)
-
-      const videoDetails = res.body as VideoDetails
+      const videoDetails = await server.videos.get({ id: videoUUID })
       expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
     }
   })
@@ -145,31 +136,33 @@ describe('Permanent live', function () {
   it('Should be able to stream again in the permanent live', async function () {
     this.timeout(20000)
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      live: {
-        enabled: true,
-        allowReplay: true,
-        maxDuration: -1,
-        transcoding: {
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        live: {
           enabled: true,
-          resolutions: getCustomConfigResolutions(false)
+          allowReplay: true,
+          maxDuration: -1,
+          transcoding: {
+            enabled: true,
+            resolutions: ConfigCommand.getCustomConfigResolutions(false)
+          }
         }
       }
     })
 
-    const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, videoUUID)
+    const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID })
 
     for (const server of servers) {
-      await waitUntilLivePublished(server.url, server.accessToken, videoUUID)
+      await server.live.waitUntilPublished({ videoId: videoUUID })
     }
 
     await checkVideoState(videoUUID, VideoState.PUBLISHED)
 
-    const count = await getPlaylistsCount(servers[0], videoUUID)
+    const count = await servers[0].live.countPlaylists({ videoUUID })
     // master playlist and 720p playlist
     expect(count).to.equal(2)
 
-    await stopFfmpeg(command)
+    await stopFfmpeg(ffmpegCommand)
   })
 
   after(async function () {
index 3d4736c8fb65e635de22779547b5f3b2bea44b7b..8f1fb78a5c37daf233989fe1f88dabcd2ff68647 100644 (file)
@@ -3,97 +3,85 @@
 import 'mocha'
 import * as chai from 'chai'
 import { FfmpegCommand } from 'fluent-ffmpeg'
-import { LiveVideoCreate, VideoDetails, VideoPrivacy, VideoState } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
-  addVideoToBlacklist,
-  checkLiveCleanup,
+  checkLiveCleanupAfterSave,
   cleanupTests,
-  createLive,
+  ConfigCommand,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getCustomConfigResolutions,
-  getVideo,
-  getVideosList,
-  removeVideo,
-  sendRTMPStreamInVideo,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
   setDefaultVideoChannel,
   stopFfmpeg,
   testFfmpegStreamError,
-  updateCustomSubConfig,
-  updateVideo,
   wait,
-  waitJobs,
-  waitUntilLiveEnded,
-  waitUntilLivePublished,
-  waitUntilLiveSaved
-} from '../../../../shared/extra-utils'
+  waitJobs
+} from '@shared/extra-utils'
+import { HttpStatusCode, LiveVideoCreate, VideoPrivacy, VideoState } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Save replay setting', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let liveVideoUUID: string
   let ffmpegCommand: FfmpegCommand
 
   async function createLiveWrapper (saveReplay: boolean) {
     if (liveVideoUUID) {
       try {
-        await removeVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+        await servers[0].videos.remove({ id: liveVideoUUID })
         await waitJobs(servers)
       } catch {}
     }
 
     const attributes: LiveVideoCreate = {
-      channelId: servers[0].videoChannel.id,
+      channelId: servers[0].store.channel.id,
       privacy: VideoPrivacy.PUBLIC,
       name: 'my super live',
       saveReplay
     }
 
-    const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
-    return res.body.video.uuid
+    const { uuid } = await servers[0].live.create({ fields: attributes })
+    return uuid
   }
 
-  async function checkVideosExist (videoId: string, existsInList: boolean, getStatus?: number) {
+  async function checkVideosExist (videoId: string, existsInList: boolean, expectedStatus?: number) {
     for (const server of servers) {
       const length = existsInList ? 1 : 0
 
-      const resVideos = await getVideosList(server.url)
-      expect(resVideos.body.data).to.have.lengthOf(length)
-      expect(resVideos.body.total).to.equal(length)
+      const { data, total } = await server.videos.list()
+      expect(data).to.have.lengthOf(length)
+      expect(total).to.equal(length)
 
-      if (getStatus) {
-        await getVideo(server.url, videoId, getStatus)
+      if (expectedStatus) {
+        await server.videos.get({ id: videoId, expectedStatus })
       }
     }
   }
 
   async function checkVideoState (videoId: string, state: VideoState) {
     for (const server of servers) {
-      const res = await getVideo(server.url, videoId)
-      expect((res.body as VideoDetails).state.id).to.equal(state)
+      const video = await server.videos.get({ id: videoId })
+      expect(video.state.id).to.equal(state)
     }
   }
 
   async function waitUntilLivePublishedOnAllServers (videoId: string) {
     for (const server of servers) {
-      await waitUntilLivePublished(server.url, server.accessToken, videoId)
+      await server.live.waitUntilPublished({ videoId })
     }
   }
 
   async function waitUntilLiveSavedOnAllServers (videoId: string) {
     for (const server of servers) {
-      await waitUntilLiveSaved(server.url, server.accessToken, videoId)
+      await server.live.waitUntilSaved({ videoId })
     }
   }
 
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -102,14 +90,16 @@ describe('Save replay setting', function () {
     // Server 1 and server 2 follow each other
     await doubleFollow(servers[0], servers[1])
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      live: {
-        enabled: true,
-        allowReplay: true,
-        maxDuration: -1,
-        transcoding: {
-          enabled: false,
-          resolutions: getCustomConfigResolutions(true)
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        live: {
+          enabled: true,
+          allowReplay: true,
+          maxDuration: -1,
+          transcoding: {
+            enabled: false,
+            resolutions: ConfigCommand.getCustomConfigResolutions(true)
+          }
         }
       }
     })
@@ -135,7 +125,7 @@ describe('Save replay setting', function () {
     it('Should correctly have updated the live and federated it when streaming in the live', async function () {
       this.timeout(30000)
 
-      ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
 
       await waitUntilLivePublishedOnAllServers(liveVideoUUID)
 
@@ -151,7 +141,7 @@ describe('Save replay setting', function () {
       await stopFfmpeg(ffmpegCommand)
 
       for (const server of servers) {
-        await waitUntilLiveEnded(server.url, server.accessToken, liveVideoUUID)
+        await server.live.waitUntilEnded({ videoId: liveVideoUUID })
       }
       await waitJobs(servers)
 
@@ -160,7 +150,7 @@ describe('Save replay setting', function () {
       await checkVideoState(liveVideoUUID, VideoState.LIVE_ENDED)
 
       // No resolutions saved since we did not save replay
-      await checkLiveCleanup(servers[0], liveVideoUUID, [])
+      await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, [])
     })
 
     it('Should correctly terminate the stream on blacklist and delete the live', async function () {
@@ -168,7 +158,7 @@ describe('Save replay setting', function () {
 
       liveVideoUUID = await createLiveWrapper(false)
 
-      ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
 
       await waitUntilLivePublishedOnAllServers(liveVideoUUID)
 
@@ -176,7 +166,7 @@ describe('Save replay setting', function () {
       await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
 
       await Promise.all([
-        addVideoToBlacklist(servers[0].url, servers[0].accessToken, liveVideoUUID, 'bad live', true),
+        servers[0].blacklist.add({ videoId: liveVideoUUID, reason: 'bad live', unfederate: true }),
         testFfmpegStreamError(ffmpegCommand, true)
       ])
 
@@ -184,12 +174,12 @@ describe('Save replay setting', function () {
 
       await checkVideosExist(liveVideoUUID, false)
 
-      await getVideo(servers[0].url, liveVideoUUID, HttpStatusCode.UNAUTHORIZED_401)
-      await getVideo(servers[1].url, liveVideoUUID, HttpStatusCode.NOT_FOUND_404)
+      await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
 
       await wait(5000)
       await waitJobs(servers)
-      await checkLiveCleanup(servers[0], liveVideoUUID, [])
+      await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, [])
     })
 
     it('Should correctly terminate the stream on delete and delete the video', async function () {
@@ -197,7 +187,7 @@ describe('Save replay setting', function () {
 
       liveVideoUUID = await createLiveWrapper(false)
 
-      ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
 
       await waitUntilLivePublishedOnAllServers(liveVideoUUID)
 
@@ -206,14 +196,14 @@ describe('Save replay setting', function () {
 
       await Promise.all([
         testFfmpegStreamError(ffmpegCommand, true),
-        removeVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+        servers[0].videos.remove({ id: liveVideoUUID })
       ])
 
       await wait(5000)
       await waitJobs(servers)
 
       await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
-      await checkLiveCleanup(servers[0], liveVideoUUID, [])
+      await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, [])
     })
   })
 
@@ -233,7 +223,7 @@ describe('Save replay setting', function () {
     it('Should correctly have updated the live and federated it when streaming in the live', async function () {
       this.timeout(20000)
 
-      ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
       await waitUntilLivePublishedOnAllServers(liveVideoUUID)
 
       await waitJobs(servers)
@@ -258,18 +248,18 @@ describe('Save replay setting', function () {
     it('Should update the saved live and correctly federate the updated attributes', async function () {
       this.timeout(30000)
 
-      await updateVideo(servers[0].url, servers[0].accessToken, liveVideoUUID, { name: 'video updated' })
+      await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated' } })
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideo(server.url, liveVideoUUID)
-        expect(res.body.name).to.equal('video updated')
-        expect(res.body.isLive).to.be.false
+        const video = await server.videos.get({ id: liveVideoUUID })
+        expect(video.name).to.equal('video updated')
+        expect(video.isLive).to.be.false
       }
     })
 
     it('Should have cleaned up the live files', async function () {
-      await checkLiveCleanup(servers[0], liveVideoUUID, [ 720 ])
+      await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, [ 720 ])
     })
 
     it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
@@ -277,14 +267,14 @@ describe('Save replay setting', function () {
 
       liveVideoUUID = await createLiveWrapper(true)
 
-      ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
       await waitUntilLivePublishedOnAllServers(liveVideoUUID)
 
       await waitJobs(servers)
       await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
 
       await Promise.all([
-        addVideoToBlacklist(servers[0].url, servers[0].accessToken, liveVideoUUID, 'bad live', true),
+        servers[0].blacklist.add({ videoId: liveVideoUUID, reason: 'bad live', unfederate: true }),
         testFfmpegStreamError(ffmpegCommand, true)
       ])
 
@@ -292,12 +282,12 @@ describe('Save replay setting', function () {
 
       await checkVideosExist(liveVideoUUID, false)
 
-      await getVideo(servers[0].url, liveVideoUUID, HttpStatusCode.UNAUTHORIZED_401)
-      await getVideo(servers[1].url, liveVideoUUID, HttpStatusCode.NOT_FOUND_404)
+      await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
 
       await wait(5000)
       await waitJobs(servers)
-      await checkLiveCleanup(servers[0], liveVideoUUID, [ 720 ])
+      await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, [ 720 ])
     })
 
     it('Should correctly terminate the stream on delete and delete the video', async function () {
@@ -305,14 +295,14 @@ describe('Save replay setting', function () {
 
       liveVideoUUID = await createLiveWrapper(true)
 
-      ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
       await waitUntilLivePublishedOnAllServers(liveVideoUUID)
 
       await waitJobs(servers)
       await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
 
       await Promise.all([
-        removeVideo(servers[0].url, servers[0].accessToken, liveVideoUUID),
+        servers[0].videos.remove({ id: liveVideoUUID }),
         testFfmpegStreamError(ffmpegCommand, true)
       ])
 
@@ -320,7 +310,7 @@ describe('Save replay setting', function () {
       await waitJobs(servers)
 
       await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
-      await checkLiveCleanup(servers[0], liveVideoUUID, [])
+      await checkLiveCleanupAfterSave(servers[0], liveVideoUUID, [])
     })
   })
 
index e00909ade3e666b3ee87f7f8adde0668e93d8781..2a1f9f108bbd86ebeb20bd9e1801e03c55f7ce62 100644 (file)
@@ -2,47 +2,42 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io'
 import { VideoPrivacy, VideoState } from '@shared/models'
 import {
   cleanupTests,
-  createLive,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getVideoIdFromUUID,
-  sendRTMPStreamInVideo,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
   setDefaultVideoChannel,
   stopFfmpeg,
-  updateCustomSubConfig,
-  viewVideo,
   wait,
   waitJobs,
-  waitUntilLiveEnded,
   waitUntilLivePublishedOnAllServers
 } from '../../../../shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test live', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
 
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      live: {
-        enabled: true,
-        allowReplay: true,
-        transcoding: {
-          enabled: false
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        live: {
+          enabled: true,
+          allowReplay: true,
+          transcoding: {
+            enabled: false
+          }
         }
       }
     })
@@ -56,12 +51,12 @@ describe('Test live', function () {
     async function createLiveWrapper () {
       const liveAttributes = {
         name: 'live video',
-        channelId: servers[0].videoChannel.id,
+        channelId: servers[0].store.channel.id,
         privacy: VideoPrivacy.PUBLIC
       }
 
-      const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
-      return res.body.video.uuid
+      const { uuid } = await servers[0].live.create({ fields: liveAttributes })
+      return uuid
     }
 
     it('Should correctly send a message when the live starts and ends', async function () {
@@ -74,22 +69,22 @@ describe('Test live', function () {
       await waitJobs(servers)
 
       {
-        const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID)
+        const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID })
 
-        const localSocket = getLiveNotificationSocket(servers[0].url)
+        const localSocket = servers[0].socketIO.getLiveNotificationSocket()
         localSocket.on('state-change', data => localStateChanges.push(data.state))
         localSocket.emit('subscribe', { videoId })
       }
 
       {
-        const videoId = await getVideoIdFromUUID(servers[1].url, liveVideoUUID)
+        const videoId = await servers[1].videos.getId({ uuid: liveVideoUUID })
 
-        const remoteSocket = getLiveNotificationSocket(servers[1].url)
+        const remoteSocket = servers[1].socketIO.getLiveNotificationSocket()
         remoteSocket.on('state-change', data => remoteStateChanges.push(data.state))
         remoteSocket.emit('subscribe', { videoId })
       }
 
-      const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
 
       await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
       await waitJobs(servers)
@@ -99,10 +94,10 @@ describe('Test live', function () {
         expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.PUBLISHED)
       }
 
-      await stopFfmpeg(command)
+      await stopFfmpeg(ffmpegCommand)
 
       for (const server of servers) {
-        await waitUntilLiveEnded(server.url, server.accessToken, liveVideoUUID)
+        await server.live.waitUntilEnded({ videoId: liveVideoUUID })
       }
       await waitJobs(servers)
 
@@ -122,22 +117,22 @@ describe('Test live', function () {
       await waitJobs(servers)
 
       {
-        const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID)
+        const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID })
 
-        const localSocket = getLiveNotificationSocket(servers[0].url)
+        const localSocket = servers[0].socketIO.getLiveNotificationSocket()
         localSocket.on('views-change', data => { localLastVideoViews = data.views })
         localSocket.emit('subscribe', { videoId })
       }
 
       {
-        const videoId = await getVideoIdFromUUID(servers[1].url, liveVideoUUID)
+        const videoId = await servers[1].videos.getId({ uuid: liveVideoUUID })
 
-        const remoteSocket = getLiveNotificationSocket(servers[1].url)
+        const remoteSocket = servers[1].socketIO.getLiveNotificationSocket()
         remoteSocket.on('views-change', data => { remoteLastVideoViews = data.views })
         remoteSocket.emit('subscribe', { videoId })
       }
 
-      const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
 
       await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
       await waitJobs(servers)
@@ -145,8 +140,8 @@ describe('Test live', function () {
       expect(localLastVideoViews).to.equal(0)
       expect(remoteLastVideoViews).to.equal(0)
 
-      await viewVideo(servers[0].url, liveVideoUUID)
-      await viewVideo(servers[1].url, liveVideoUUID)
+      await servers[0].videos.view({ id: liveVideoUUID })
+      await servers[1].videos.view({ id: liveVideoUUID })
 
       await waitJobs(servers)
       await wait(5000)
@@ -155,7 +150,7 @@ describe('Test live', function () {
       expect(localLastVideoViews).to.equal(2)
       expect(remoteLastVideoViews).to.equal(2)
 
-      await stopFfmpeg(command)
+      await stopFfmpeg(ffmpegCommand)
     })
 
     it('Should not receive a notification after unsubscribe', async function () {
@@ -166,13 +161,13 @@ describe('Test live', function () {
       const liveVideoUUID = await createLiveWrapper()
       await waitJobs(servers)
 
-      const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID)
+      const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID })
 
-      const socket = getLiveNotificationSocket(servers[0].url)
+      const socket = servers[0].socketIO.getLiveNotificationSocket()
       socket.on('state-change', data => stateChanges.push(data.state))
       socket.emit('subscribe', { videoId })
 
-      const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      const command = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
 
       await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
       await waitJobs(servers)
index a44d21ffa45cd7c4503a48130f3806e7e67fdac4..5e3a79c6415f9511e33fcc8f501bfabbb47dfe34 100644 (file)
@@ -3,20 +3,15 @@
 import 'mocha'
 import * as chai from 'chai'
 import { FfmpegCommand } from 'fluent-ffmpeg'
-import { VideoDetails, VideoPrivacy } from '@shared/models'
+import { VideoPrivacy } from '@shared/models'
 import {
   cleanupTests,
-  createLive,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getVideo,
-  sendRTMPStreamInVideo,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
   setDefaultVideoChannel,
   stopFfmpeg,
-  updateCustomSubConfig,
-  viewVideo,
   wait,
   waitJobs,
   waitUntilLivePublishedOnAllServers
@@ -25,23 +20,25 @@ import {
 const expect = chai.expect
 
 describe('Test live', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
 
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      live: {
-        enabled: true,
-        allowReplay: true,
-        transcoding: {
-          enabled: false
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        live: {
+          enabled: true,
+          allowReplay: true,
+          transcoding: {
+            enabled: false
+          }
         }
       }
     })
@@ -56,9 +53,7 @@ describe('Test live', function () {
 
     async function countViews (expected: number) {
       for (const server of servers) {
-        const res = await getVideo(server.url, liveVideoId)
-        const video: VideoDetails = res.body
-
+        const video = await server.videos.get({ id: liveVideoId })
         expect(video.views).to.equal(expected)
       }
     }
@@ -68,14 +63,14 @@ describe('Test live', function () {
 
       const liveAttributes = {
         name: 'live video',
-        channelId: servers[0].videoChannel.id,
+        channelId: servers[0].store.channel.id,
         privacy: VideoPrivacy.PUBLIC
       }
 
-      const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
-      liveVideoId = res.body.video.uuid
+      const live = await servers[0].live.create({ fields: liveAttributes })
+      liveVideoId = live.uuid
 
-      command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
+      command = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId })
       await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
       await waitJobs(servers)
     })
@@ -87,8 +82,8 @@ describe('Test live', function () {
     it('Should view a live twice and display 1 view', async function () {
       this.timeout(30000)
 
-      await viewVideo(servers[0].url, liveVideoId)
-      await viewVideo(servers[0].url, liveVideoId)
+      await servers[0].videos.view({ id: liveVideoId })
+      await servers[0].videos.view({ id: liveVideoId })
 
       await wait(7000)
 
@@ -109,9 +104,9 @@ describe('Test live', function () {
     it('Should view a live on a remote and on local and display 2 views', async function () {
       this.timeout(30000)
 
-      await viewVideo(servers[0].url, liveVideoId)
-      await viewVideo(servers[1].url, liveVideoId)
-      await viewVideo(servers[1].url, liveVideoId)
+      await servers[0].videos.view({ id: liveVideoId })
+      await servers[1].videos.view({ id: liveVideoId })
+      await servers[1].videos.view({ id: liveVideoId })
 
       await wait(7000)
       await waitJobs(servers)
index 50397924e0417f3556ee717a773f123adbd87010..d555cff194ff89c533fbf077c6e5e136de41fbda 100644 (file)
@@ -2,75 +2,70 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { join } from 'path'
+import { basename, join } from 'path'
 import { ffprobePromise, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils'
-import { LiveVideo, LiveVideoCreate, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
-  addVideoToBlacklist,
-  buildServerDirectory,
-  checkLiveCleanup,
+  checkLiveCleanupAfterSave,
   checkLiveSegmentHash,
   checkResolutionsInMasterPlaylist,
   cleanupTests,
-  createLive,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getLive,
-  getMyVideosWithFilter,
-  getPlaylist,
-  getVideo,
-  getVideosList,
-  getVideosWithFilters,
   killallServers,
+  LiveCommand,
   makeRawRequest,
-  removeVideo,
-  reRunServer,
+  PeerTubeServer,
   sendRTMPStream,
-  sendRTMPStreamInVideo,
-  ServerInfo,
   setAccessTokensToServers,
   setDefaultVideoChannel,
   stopFfmpeg,
   testFfmpegStreamError,
   testImage,
-  updateCustomSubConfig,
-  updateLive,
-  uploadVideoAndGetId,
   wait,
   waitJobs,
-  waitUntilLiveEnded,
-  waitUntilLivePublished,
-  waitUntilLivePublishedOnAllServers,
-  waitUntilLiveSegmentGeneration
-} from '../../../../shared/extra-utils'
+  waitUntilLivePublishedOnAllServers
+} from '@shared/extra-utils'
+import {
+  HttpStatusCode,
+  LiveVideo,
+  LiveVideoCreate,
+  VideoDetails,
+  VideoPrivacy,
+  VideoState,
+  VideoStreamingPlaylistType
+} from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test live', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
+  let commands: LiveCommand[]
 
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      live: {
-        enabled: true,
-        allowReplay: true,
-        transcoding: {
-          enabled: false
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        live: {
+          enabled: true,
+          allowReplay: true,
+          transcoding: {
+            enabled: false
+          }
         }
       }
     })
 
     // Server 1 and server 2 follow each other
     await doubleFollow(servers[0], servers[1])
+
+    commands = servers.map(s => s.live)
   })
 
   describe('Live creation, update and delete', function () {
@@ -85,7 +80,7 @@ describe('Test live', function () {
         language: 'fr',
         description: 'super live description',
         support: 'support field',
-        channelId: servers[0].videoChannel.id,
+        channelId: servers[0].store.channel.id,
         nsfw: false,
         waitTranscoding: false,
         name: 'my super live',
@@ -98,14 +93,13 @@ describe('Test live', function () {
         thumbnailfile: 'video_short1.webm.jpg'
       }
 
-      const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
-      liveVideoUUID = res.body.video.uuid
+      const live = await commands[0].create({ fields: attributes })
+      liveVideoUUID = live.uuid
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const resVideo = await getVideo(server.url, liveVideoUUID)
-        const video: VideoDetails = resVideo.body
+        const video = await server.videos.get({ id: liveVideoUUID })
 
         expect(video.category.id).to.equal(1)
         expect(video.licence.id).to.equal(2)
@@ -113,8 +107,8 @@ describe('Test live', function () {
         expect(video.description).to.equal('super live description')
         expect(video.support).to.equal('support field')
 
-        expect(video.channel.name).to.equal(servers[0].videoChannel.name)
-        expect(video.channel.host).to.equal(servers[0].videoChannel.host)
+        expect(video.channel.name).to.equal(servers[0].store.channel.name)
+        expect(video.channel.host).to.equal(servers[0].store.channel.host)
 
         expect(video.isLive).to.be.true
 
@@ -129,8 +123,7 @@ describe('Test live', function () {
         await testImage(server.url, 'video_short1-preview.webm', video.previewPath)
         await testImage(server.url, 'video_short1.webm', video.thumbnailPath)
 
-        const resLive = await getLive(server.url, server.accessToken, liveVideoUUID)
-        const live: LiveVideo = resLive.body
+        const live = await server.live.get({ videoId: liveVideoUUID })
 
         if (server.url === servers[0].url) {
           expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live')
@@ -149,20 +142,18 @@ describe('Test live', function () {
 
       const attributes: LiveVideoCreate = {
         name: 'default live thumbnail',
-        channelId: servers[0].videoChannel.id,
+        channelId: servers[0].store.channel.id,
         privacy: VideoPrivacy.UNLISTED,
         nsfw: true
       }
 
-      const res = await createLive(servers[0].url, servers[0].accessToken, attributes)
-      const videoId = res.body.video.uuid
+      const live = await commands[0].create({ fields: attributes })
+      const videoId = live.uuid
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const resVideo = await getVideo(server.url, videoId)
-        const video: VideoDetails = resVideo.body
-
+        const video = await server.videos.get({ id: videoId })
         expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
         expect(video.nsfw).to.be.true
 
@@ -173,28 +164,27 @@ describe('Test live', function () {
 
     it('Should not have the live listed since nobody streams into', async function () {
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { total, data } = await server.videos.list()
 
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
+        expect(total).to.equal(0)
+        expect(data).to.have.lengthOf(0)
       }
     })
 
     it('Should not be able to update a live of another server', async function () {
-      await updateLive(servers[1].url, servers[1].accessToken, liveVideoUUID, { saveReplay: false }, HttpStatusCode.FORBIDDEN_403)
+      await commands[1].update({ videoId: liveVideoUUID, fields: { saveReplay: false }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should update the live', async function () {
       this.timeout(10000)
 
-      await updateLive(servers[0].url, servers[0].accessToken, liveVideoUUID, { saveReplay: false })
+      await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false } })
       await waitJobs(servers)
     })
 
     it('Have the live updated', async function () {
       for (const server of servers) {
-        const res = await getLive(server.url, server.accessToken, liveVideoUUID)
-        const live: LiveVideo = res.body
+        const live = await server.live.get({ videoId: liveVideoUUID })
 
         if (server.url === servers[0].url) {
           expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live')
@@ -211,77 +201,75 @@ describe('Test live', function () {
     it('Delete the live', async function () {
       this.timeout(10000)
 
-      await removeVideo(servers[0].url, servers[0].accessToken, liveVideoUUID)
+      await servers[0].videos.remove({ id: liveVideoUUID })
       await waitJobs(servers)
     })
 
     it('Should have the live deleted', async function () {
       for (const server of servers) {
-        await getVideo(server.url, liveVideoUUID, HttpStatusCode.NOT_FOUND_404)
-        await getLive(server.url, server.accessToken, liveVideoUUID, HttpStatusCode.NOT_FOUND_404)
+        await server.videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+        await server.live.get({ videoId: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
       }
     })
   })
 
   describe('Live filters', function () {
-    let command: any
+    let ffmpegCommand: any
     let liveVideoId: string
     let vodVideoId: string
 
     before(async function () {
       this.timeout(120000)
 
-      vodVideoId = (await uploadVideoAndGetId({ server: servers[0], videoName: 'vod video' })).uuid
+      vodVideoId = (await servers[0].videos.quickUpload({ name: 'vod video' })).uuid
 
-      const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].videoChannel.id }
-      const resLive = await createLive(servers[0].url, servers[0].accessToken, liveOptions)
-      liveVideoId = resLive.body.video.uuid
+      const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].store.channel.id }
+      const live = await commands[0].create({ fields: liveOptions })
+      liveVideoId = live.uuid
 
-      command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
+      ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId })
       await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
       await waitJobs(servers)
     })
 
     it('Should only display lives', async function () {
-      const res = await getVideosWithFilters(servers[0].url, { isLive: true })
+      const { data, total } = await servers[0].videos.list({ isLive: true })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].name).to.equal('live')
+      expect(total).to.equal(1)
+      expect(data).to.have.lengthOf(1)
+      expect(data[0].name).to.equal('live')
     })
 
     it('Should not display lives', async function () {
-      const res = await getVideosWithFilters(servers[0].url, { isLive: false })
+      const { data, total } = await servers[0].videos.list({ isLive: false })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].name).to.equal('vod video')
+      expect(total).to.equal(1)
+      expect(data).to.have.lengthOf(1)
+      expect(data[0].name).to.equal('vod video')
     })
 
     it('Should display my lives', async function () {
       this.timeout(60000)
 
-      await stopFfmpeg(command)
+      await stopFfmpeg(ffmpegCommand)
       await waitJobs(servers)
 
-      const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: true })
-      const videos = res.body.data as Video[]
+      const { data } = await servers[0].videos.listMyVideos({ isLive: true })
 
-      const result = videos.every(v => v.isLive)
+      const result = data.every(v => v.isLive)
       expect(result).to.be.true
     })
 
     it('Should not display my lives', async function () {
-      const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: false })
-      const videos = res.body.data as Video[]
+      const { data } = await servers[0].videos.listMyVideos({ isLive: false })
 
-      const result = videos.every(v => !v.isLive)
+      const result = data.every(v => !v.isLive)
       expect(result).to.be.true
     })
 
     after(async function () {
-      await removeVideo(servers[0].url, servers[0].accessToken, vodVideoId)
-      await removeVideo(servers[0].url, servers[0].accessToken, liveVideoId)
+      await servers[0].videos.remove({ id: vodVideoId })
+      await servers[0].videos.remove({ id: liveVideoId })
     })
   })
 
@@ -296,18 +284,17 @@ describe('Test live', function () {
     async function createLiveWrapper () {
       const liveAttributes = {
         name: 'user live',
-        channelId: servers[0].videoChannel.id,
+        channelId: servers[0].store.channel.id,
         privacy: VideoPrivacy.PUBLIC,
         saveReplay: false
       }
 
-      const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
-      const uuid = res.body.video.uuid
+      const { uuid } = await commands[0].create({ fields: liveAttributes })
 
-      const resLive = await getLive(servers[0].url, servers[0].accessToken, uuid)
-      const resVideo = await getVideo(servers[0].url, uuid)
+      const live = await commands[0].get({ videoId: uuid })
+      const video = await servers[0].videos.get({ id: uuid })
 
-      return Object.assign(resVideo.body, resLive.body) as LiveVideo & VideoDetails
+      return Object.assign(video, live)
     }
 
     it('Should not allow a stream without the appropriate path', async function () {
@@ -335,13 +322,12 @@ describe('Test live', function () {
 
     it('Should list this live now someone stream into it', async function () {
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { total, data } = await server.videos.list()
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
-
-        const video: Video = res.body.data[0]
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
 
+        const video = data[0]
         expect(video.name).to.equal('user live')
         expect(video.isLive).to.be.true
       }
@@ -352,7 +338,7 @@ describe('Test live', function () {
 
       liveVideo = await createLiveWrapper()
 
-      await addVideoToBlacklist(servers[0].url, servers[0].accessToken, liveVideo.uuid)
+      await servers[0].blacklist.add({ videoId: liveVideo.uuid })
 
       const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey)
       await testFfmpegStreamError(command, true)
@@ -363,7 +349,7 @@ describe('Test live', function () {
 
       liveVideo = await createLiveWrapper()
 
-      await removeVideo(servers[0].url, servers[0].accessToken, liveVideo.uuid)
+      await servers[0].videos.remove({ id: liveVideo.uuid })
 
       const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey)
       await testFfmpegStreamError(command, true)
@@ -376,24 +362,21 @@ describe('Test live', function () {
     async function createLiveWrapper (saveReplay: boolean) {
       const liveAttributes = {
         name: 'live video',
-        channelId: servers[0].videoChannel.id,
+        channelId: servers[0].store.channel.id,
         privacy: VideoPrivacy.PUBLIC,
         saveReplay
       }
 
-      const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
-      return res.body.video.uuid
+      const { uuid } = await commands[0].create({ fields: liveAttributes })
+      return uuid
     }
 
     async function testVideoResolutions (liveVideoId: string, resolutions: number[]) {
       for (const server of servers) {
-        const resList = await getVideosList(server.url)
-        const videos: Video[] = resList.body.data
-
-        expect(videos.find(v => v.uuid === liveVideoId)).to.exist
+        const { data } = await server.videos.list()
+        expect(data.find(v => v.uuid === liveVideoId)).to.exist
 
-        const resVideo = await getVideo(server.url, liveVideoId)
-        const video: VideoDetails = resVideo.body
+        const video = await server.videos.get({ id: liveVideoId })
 
         expect(video.streamingPlaylists).to.have.lengthOf(1)
 
@@ -403,39 +386,48 @@ describe('Test live', function () {
         // Only finite files are displayed
         expect(hlsPlaylist.files).to.have.lengthOf(0)
 
-        await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions)
+        await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
 
         for (let i = 0; i < resolutions.length; i++) {
           const segmentNum = 3
           const segmentName = `${i}-00000${segmentNum}.ts`
-          await waitUntilLiveSegmentGeneration(servers[0], video.uuid, i, segmentNum)
+          await commands[0].waitUntilSegmentGeneration({ videoUUID: video.uuid, resolution: i, segment: segmentNum })
 
-          const res = await getPlaylist(`${servers[0].url}/static/streaming-playlists/hls/${video.uuid}/${i}.m3u8`)
-          const subPlaylist = res.text
+          const subPlaylist = await servers[0].streamingPlaylists.get({
+            url: `${servers[0].url}/static/streaming-playlists/hls/${video.uuid}/${i}.m3u8`
+          })
 
           expect(subPlaylist).to.contain(segmentName)
 
           const baseUrlAndPath = servers[0].url + '/static/streaming-playlists/hls'
-          await checkLiveSegmentHash(baseUrlAndPath, video.uuid, segmentName, hlsPlaylist)
+          await checkLiveSegmentHash({
+            server,
+            baseUrlSegment: baseUrlAndPath,
+            videoUUID: video.uuid,
+            segmentName,
+            hlsPlaylist
+          })
         }
       }
     }
 
     function updateConf (resolutions: number[]) {
-      return updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-        live: {
-          enabled: true,
-          allowReplay: true,
-          maxDuration: -1,
-          transcoding: {
+      return servers[0].config.updateCustomSubConfig({
+        newConfig: {
+          live: {
             enabled: true,
-            resolutions: {
-              '240p': resolutions.includes(240),
-              '360p': resolutions.includes(360),
-              '480p': resolutions.includes(480),
-              '720p': resolutions.includes(720),
-              '1080p': resolutions.includes(1080),
-              '2160p': resolutions.includes(2160)
+            allowReplay: true,
+            maxDuration: -1,
+            transcoding: {
+              enabled: true,
+              resolutions: {
+                '240p': resolutions.includes(240),
+                '360p': resolutions.includes(360),
+                '480p': resolutions.includes(480),
+                '720p': resolutions.includes(720),
+                '1080p': resolutions.includes(1080),
+                '2160p': resolutions.includes(2160)
+              }
             }
           }
         }
@@ -451,13 +443,13 @@ describe('Test live', function () {
 
       liveVideoId = await createLiveWrapper(false)
 
-      const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
+      const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId })
       await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
       await waitJobs(servers)
 
       await testVideoResolutions(liveVideoId, [ 720 ])
 
-      await stopFfmpeg(command)
+      await stopFfmpeg(ffmpegCommand)
     })
 
     it('Should enable transcoding with some resolutions', async function () {
@@ -467,13 +459,13 @@ describe('Test live', function () {
       await updateConf(resolutions)
       liveVideoId = await createLiveWrapper(false)
 
-      const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
+      const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId })
       await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
       await waitJobs(servers)
 
       await testVideoResolutions(liveVideoId, resolutions)
 
-      await stopFfmpeg(command)
+      await stopFfmpeg(ffmpegCommand)
     })
 
     it('Should enable transcoding with some resolutions and correctly save them', async function () {
@@ -484,14 +476,14 @@ describe('Test live', function () {
       await updateConf(resolutions)
       liveVideoId = await createLiveWrapper(true)
 
-      const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId, 'video_short2.webm')
+      const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
       await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
       await waitJobs(servers)
 
       await testVideoResolutions(liveVideoId, resolutions)
 
-      await stopFfmpeg(command)
-      await waitUntilLiveEnded(servers[0].url, servers[0].accessToken, liveVideoId)
+      await stopFfmpeg(ffmpegCommand)
+      await commands[0].waitUntilEnded({ videoId: liveVideoId })
 
       await waitJobs(servers)
 
@@ -504,8 +496,7 @@ describe('Test live', function () {
       }
 
       for (const server of servers) {
-        const resVideo = await getVideo(server.url, liveVideoId)
-        const video: VideoDetails = resVideo.body
+        const video = await server.videos.get({ id: liveVideoId })
 
         expect(video.state.id).to.equal(VideoState.PUBLISHED)
         expect(video.duration).to.be.greaterThan(1)
@@ -515,6 +506,10 @@ describe('Test live', function () {
         await makeRawRequest(hlsPlaylist.playlistUrl, HttpStatusCode.OK_200)
         await makeRawRequest(hlsPlaylist.segmentsSha256Url, HttpStatusCode.OK_200)
 
+        // We should have generated random filenames
+        expect(basename(hlsPlaylist.playlistUrl)).to.not.equal('master.m3u8')
+        expect(basename(hlsPlaylist.segmentsSha256Url)).to.not.equal('segments-sha256.json')
+
         expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length)
 
         for (const resolution of resolutions) {
@@ -529,8 +524,10 @@ describe('Test live', function () {
             expect(file.fps).to.be.approximately(30, 2)
           }
 
-          const filename = `${video.uuid}-${resolution}-fragmented.mp4`
-          const segmentPath = buildServerDirectory(servers[0], join('streaming-playlists', 'hls', video.uuid, filename))
+          const filename = basename(file.fileUrl)
+          expect(filename).to.not.contain(video.uuid)
+
+          const segmentPath = servers[0].servers.buildDirectory(join('streaming-playlists', 'hls', video.uuid, filename))
 
           const probe = await ffprobePromise(segmentPath)
           const videoStream = await getVideoStreamFromFile(segmentPath, probe)
@@ -546,7 +543,7 @@ describe('Test live', function () {
     it('Should correctly have cleaned up the live files', async function () {
       this.timeout(30000)
 
-      await checkLiveCleanup(servers[0], liveVideoId, [ 240, 360, 720 ])
+      await checkLiveCleanupAfterSave(servers[0], liveVideoId, [ 240, 360, 720 ])
     })
   })
 
@@ -557,13 +554,13 @@ describe('Test live', function () {
     async function createLiveWrapper (saveReplay: boolean) {
       const liveAttributes = {
         name: 'live video',
-        channelId: servers[0].videoChannel.id,
+        channelId: servers[0].store.channel.id,
         privacy: VideoPrivacy.PUBLIC,
         saveReplay
       }
 
-      const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
-      return res.body.video.uuid
+      const { uuid } = await commands[0].create({ fields: liveAttributes })
+      return uuid
     }
 
     before(async function () {
@@ -573,20 +570,20 @@ describe('Test live', function () {
       liveVideoReplayId = await createLiveWrapper(true)
 
       await Promise.all([
-        sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId),
-        sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoReplayId)
+        commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }),
+        commands[0].sendRTMPStreamInVideo({ videoId: liveVideoReplayId })
       ])
 
       await Promise.all([
-        waitUntilLivePublished(servers[0].url, servers[0].accessToken, liveVideoId),
-        waitUntilLivePublished(servers[0].url, servers[0].accessToken, liveVideoReplayId)
+        commands[0].waitUntilPublished({ videoId: liveVideoId }),
+        commands[0].waitUntilPublished({ videoId: liveVideoReplayId })
       ])
 
-      await waitUntilLiveSegmentGeneration(servers[0], liveVideoId, 0, 2)
-      await waitUntilLiveSegmentGeneration(servers[0], liveVideoReplayId, 0, 2)
+      await commands[0].waitUntilSegmentGeneration({ videoUUID: liveVideoId, resolution: 0, segment: 2 })
+      await commands[0].waitUntilSegmentGeneration({ videoUUID: liveVideoReplayId, resolution: 0, segment: 2 })
 
       await killallServers([ servers[0] ])
-      await reRunServer(servers[0])
+      await servers[0].run()
 
       await wait(5000)
     })
@@ -594,13 +591,13 @@ describe('Test live', function () {
     it('Should cleanup lives', async function () {
       this.timeout(60000)
 
-      await waitUntilLiveEnded(servers[0].url, servers[0].accessToken, liveVideoId)
+      await commands[0].waitUntilEnded({ videoId: liveVideoId })
     })
 
     it('Should save a live replay', async function () {
       this.timeout(120000)
 
-      await waitUntilLivePublished(servers[0].url, servers[0].accessToken, liveVideoReplayId)
+      await commands[0].waitUntilPublished({ videoId: liveVideoReplayId })
     })
   })
 
index fb765e7e30768ee75d5e90771b347ea7a4a9c19f..c258414ce3c39145a7fd87dbffbfc901ef533af4 100644 (file)
@@ -3,70 +3,37 @@
 import 'mocha'
 import * as chai from 'chai'
 import {
-  AbuseFilter,
-  AbuseMessage,
-  AbusePredefinedReasonsString,
-  AbuseState,
-  Account,
-  AdminAbuse,
-  UserAbuse,
-  VideoComment
-} from '@shared/models'
-import {
-  addAbuseMessage,
-  addVideoCommentThread,
+  AbusesCommand,
   cleanupTests,
-  createUser,
-  deleteAbuse,
-  deleteAbuseMessage,
-  deleteVideoComment,
-  flushAndRunMultipleServers,
-  generateUserAccessToken,
-  getAccount,
-  getAdminAbusesList,
-  getUserAbusesList,
-  getVideoCommentThreads,
-  getVideoIdFromUUID,
-  getVideosList,
-  immutableAssign,
-  listAbuseMessages,
-  removeUser,
-  removeVideo,
-  reportAbuse,
-  ServerInfo,
+  createMultipleServers,
+  doubleFollow,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updateAbuse,
-  uploadVideo,
-  uploadVideoAndGetId,
-  userLogin
-} from '../../../../shared/extra-utils/index'
-import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import {
-  addAccountToServerBlocklist,
-  addServerToServerBlocklist,
-  removeAccountFromServerBlocklist,
-  removeServerFromServerBlocklist
-} from '../../../../shared/extra-utils/users/blocklist'
+  waitJobs
+} from '@shared/extra-utils'
+import { AbuseMessage, AbusePredefinedReasonsString, AbuseState, AdminAbuse, UserAbuse } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test abuses', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let abuseServer1: AdminAbuse
   let abuseServer2: AdminAbuse
+  let commands: AbusesCommand[]
 
   before(async function () {
     this.timeout(50000)
 
     // Run servers
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
 
     // Server 1 and server 2 follow each other
     await doubleFollow(servers[0], servers[1])
+
+    commands = servers.map(s => s.abuses)
   })
 
   describe('Video abuses', function () {
@@ -75,179 +42,189 @@ describe('Test abuses', function () {
       this.timeout(50000)
 
       // Upload some videos on each servers
-      const video1Attributes = {
-        name: 'my super name for server 1',
-        description: 'my super description for server 1'
+      {
+        const attributes = {
+          name: 'my super name for server 1',
+          description: 'my super description for server 1'
+        }
+        await servers[0].videos.upload({ attributes })
       }
-      await uploadVideo(servers[0].url, servers[0].accessToken, video1Attributes)
 
-      const video2Attributes = {
-        name: 'my super name for server 2',
-        description: 'my super description for server 2'
+      {
+        const attributes = {
+          name: 'my super name for server 2',
+          description: 'my super description for server 2'
+        }
+        await servers[1].videos.upload({ attributes })
       }
-      await uploadVideo(servers[1].url, servers[1].accessToken, video2Attributes)
 
       // Wait videos propagation, server 2 has transcoding enabled
       await waitJobs(servers)
 
-      const res = await getVideosList(servers[0].url)
-      const videos = res.body.data
+      const { data } = await servers[0].videos.list()
+      expect(data.length).to.equal(2)
 
-      expect(videos.length).to.equal(2)
-
-      servers[0].video = videos.find(video => video.name === 'my super name for server 1')
-      servers[1].video = videos.find(video => video.name === 'my super name for server 2')
+      servers[0].store.videoCreated = data.find(video => video.name === 'my super name for server 1')
+      servers[1].store.videoCreated = data.find(video => video.name === 'my super name for server 2')
     })
 
     it('Should not have abuses', async function () {
-      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+      const body = await commands[0].getAdminList()
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data.length).to.equal(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data.length).to.equal(0)
     })
 
     it('Should report abuse on a local video', async function () {
       this.timeout(15000)
 
       const reason = 'my super bad reason'
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: servers[0].video.id, reason })
+      await commands[0].report({ videoId: servers[0].store.videoCreated.id, reason })
 
       // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2
       await waitJobs(servers)
     })
 
     it('Should have 1 video abuses on server 1 and 0 on server 2', async function () {
-      const res1 = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+      {
+        const body = await commands[0].getAdminList()
 
-      expect(res1.body.total).to.equal(1)
-      expect(res1.body.data).to.be.an('array')
-      expect(res1.body.data.length).to.equal(1)
+        expect(body.total).to.equal(1)
+        expect(body.data).to.be.an('array')
+        expect(body.data.length).to.equal(1)
 
-      const abuse: AdminAbuse = res1.body.data[0]
-      expect(abuse.reason).to.equal('my super bad reason')
+        const abuse = body.data[0]
+        expect(abuse.reason).to.equal('my super bad reason')
 
-      expect(abuse.reporterAccount.name).to.equal('root')
-      expect(abuse.reporterAccount.host).to.equal(servers[0].host)
+        expect(abuse.reporterAccount.name).to.equal('root')
+        expect(abuse.reporterAccount.host).to.equal(servers[0].host)
 
-      expect(abuse.video.id).to.equal(servers[0].video.id)
-      expect(abuse.video.channel).to.exist
+        expect(abuse.video.id).to.equal(servers[0].store.videoCreated.id)
+        expect(abuse.video.channel).to.exist
 
-      expect(abuse.comment).to.be.null
+        expect(abuse.comment).to.be.null
 
-      expect(abuse.flaggedAccount.name).to.equal('root')
-      expect(abuse.flaggedAccount.host).to.equal(servers[0].host)
+        expect(abuse.flaggedAccount.name).to.equal('root')
+        expect(abuse.flaggedAccount.host).to.equal(servers[0].host)
 
-      expect(abuse.video.countReports).to.equal(1)
-      expect(abuse.video.nthReport).to.equal(1)
+        expect(abuse.video.countReports).to.equal(1)
+        expect(abuse.video.nthReport).to.equal(1)
 
-      expect(abuse.countReportsForReporter).to.equal(1)
-      expect(abuse.countReportsForReportee).to.equal(1)
+        expect(abuse.countReportsForReporter).to.equal(1)
+        expect(abuse.countReportsForReportee).to.equal(1)
+      }
 
-      const res2 = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken })
-      expect(res2.body.total).to.equal(0)
-      expect(res2.body.data).to.be.an('array')
-      expect(res2.body.data.length).to.equal(0)
+      {
+        const body = await commands[1].getAdminList()
+        expect(body.total).to.equal(0)
+        expect(body.data).to.be.an('array')
+        expect(body.data.length).to.equal(0)
+      }
     })
 
     it('Should report abuse on a remote video', async function () {
       this.timeout(10000)
 
       const reason = 'my super bad reason 2'
-      const videoId = await getVideoIdFromUUID(servers[0].url, servers[1].video.uuid)
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId, reason })
+      const videoId = await servers[0].videos.getId({ uuid: servers[1].store.videoCreated.uuid })
+      await commands[0].report({ videoId, reason })
 
       // We wait requests propagation
       await waitJobs(servers)
     })
 
     it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
-      const res1 = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+      {
+        const body = await commands[0].getAdminList()
 
-      expect(res1.body.total).to.equal(2)
-      expect(res1.body.data.length).to.equal(2)
+        expect(body.total).to.equal(2)
+        expect(body.data.length).to.equal(2)
 
-      const abuse1: AdminAbuse = res1.body.data[0]
-      expect(abuse1.reason).to.equal('my super bad reason')
-      expect(abuse1.reporterAccount.name).to.equal('root')
-      expect(abuse1.reporterAccount.host).to.equal(servers[0].host)
+        const abuse1 = body.data[0]
+        expect(abuse1.reason).to.equal('my super bad reason')
+        expect(abuse1.reporterAccount.name).to.equal('root')
+        expect(abuse1.reporterAccount.host).to.equal(servers[0].host)
 
-      expect(abuse1.video.id).to.equal(servers[0].video.id)
-      expect(abuse1.video.countReports).to.equal(1)
-      expect(abuse1.video.nthReport).to.equal(1)
+        expect(abuse1.video.id).to.equal(servers[0].store.videoCreated.id)
+        expect(abuse1.video.countReports).to.equal(1)
+        expect(abuse1.video.nthReport).to.equal(1)
 
-      expect(abuse1.comment).to.be.null
+        expect(abuse1.comment).to.be.null
 
-      expect(abuse1.flaggedAccount.name).to.equal('root')
-      expect(abuse1.flaggedAccount.host).to.equal(servers[0].host)
+        expect(abuse1.flaggedAccount.name).to.equal('root')
+        expect(abuse1.flaggedAccount.host).to.equal(servers[0].host)
 
-      expect(abuse1.state.id).to.equal(AbuseState.PENDING)
-      expect(abuse1.state.label).to.equal('Pending')
-      expect(abuse1.moderationComment).to.be.null
+        expect(abuse1.state.id).to.equal(AbuseState.PENDING)
+        expect(abuse1.state.label).to.equal('Pending')
+        expect(abuse1.moderationComment).to.be.null
 
-      const abuse2: AdminAbuse = res1.body.data[1]
-      expect(abuse2.reason).to.equal('my super bad reason 2')
+        const abuse2 = body.data[1]
+        expect(abuse2.reason).to.equal('my super bad reason 2')
 
-      expect(abuse2.reporterAccount.name).to.equal('root')
-      expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
+        expect(abuse2.reporterAccount.name).to.equal('root')
+        expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
 
-      expect(abuse2.video.id).to.equal(servers[1].video.id)
+        expect(abuse2.video.id).to.equal(servers[1].store.videoCreated.id)
 
-      expect(abuse2.comment).to.be.null
+        expect(abuse2.comment).to.be.null
 
-      expect(abuse2.flaggedAccount.name).to.equal('root')
-      expect(abuse2.flaggedAccount.host).to.equal(servers[1].host)
+        expect(abuse2.flaggedAccount.name).to.equal('root')
+        expect(abuse2.flaggedAccount.host).to.equal(servers[1].host)
 
-      expect(abuse2.state.id).to.equal(AbuseState.PENDING)
-      expect(abuse2.state.label).to.equal('Pending')
-      expect(abuse2.moderationComment).to.be.null
+        expect(abuse2.state.id).to.equal(AbuseState.PENDING)
+        expect(abuse2.state.label).to.equal('Pending')
+        expect(abuse2.moderationComment).to.be.null
+      }
 
-      const res2 = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken })
-      expect(res2.body.total).to.equal(1)
-      expect(res2.body.data.length).to.equal(1)
+      {
+        const body = await commands[1].getAdminList()
+        expect(body.total).to.equal(1)
+        expect(body.data.length).to.equal(1)
 
-      abuseServer2 = res2.body.data[0]
-      expect(abuseServer2.reason).to.equal('my super bad reason 2')
-      expect(abuseServer2.reporterAccount.name).to.equal('root')
-      expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
+        abuseServer2 = body.data[0]
+        expect(abuseServer2.reason).to.equal('my super bad reason 2')
+        expect(abuseServer2.reporterAccount.name).to.equal('root')
+        expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
 
-      expect(abuse2.flaggedAccount.name).to.equal('root')
-      expect(abuse2.flaggedAccount.host).to.equal(servers[1].host)
+        expect(abuseServer2.flaggedAccount.name).to.equal('root')
+        expect(abuseServer2.flaggedAccount.host).to.equal(servers[1].host)
 
-      expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
-      expect(abuseServer2.state.label).to.equal('Pending')
-      expect(abuseServer2.moderationComment).to.be.null
+        expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
+        expect(abuseServer2.state.label).to.equal('Pending')
+        expect(abuseServer2.moderationComment).to.be.null
+      }
     })
 
     it('Should hide video abuses from blocked accounts', async function () {
       this.timeout(10000)
 
       {
-        const videoId = await getVideoIdFromUUID(servers[1].url, servers[0].video.uuid)
-        await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId, reason: 'will mute this' })
+        const videoId = await servers[1].videos.getId({ uuid: servers[0].store.videoCreated.uuid })
+        await commands[1].report({ videoId, reason: 'will mute this' })
         await waitJobs(servers)
 
-        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
-        expect(res.body.total).to.equal(3)
+        const body = await commands[0].getAdminList()
+        expect(body.total).to.equal(3)
       }
 
       const accountToBlock = 'root@' + servers[1].host
 
       {
-        await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
+        await servers[0].blocklist.addToServerBlocklist({ account: accountToBlock })
 
-        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
-        expect(res.body.total).to.equal(2)
+        const body = await commands[0].getAdminList()
+        expect(body.total).to.equal(2)
 
-        const abuse = res.body.data.find(a => a.reason === 'will mute this')
+        const abuse = body.data.find(a => a.reason === 'will mute this')
         expect(abuse).to.be.undefined
       }
 
       {
-        await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
+        await servers[0].blocklist.removeFromServerBlocklist({ account: accountToBlock })
 
-        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
-        expect(res.body.total).to.equal(3)
+        const body = await commands[0].getAdminList()
+        expect(body.total).to.equal(3)
       }
     })
 
@@ -255,35 +232,35 @@ describe('Test abuses', function () {
       const serverToBlock = servers[1].host
 
       {
-        await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host)
+        await servers[0].blocklist.addToServerBlocklist({ server: serverToBlock })
 
-        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
-        expect(res.body.total).to.equal(2)
+        const body = await commands[0].getAdminList()
+        expect(body.total).to.equal(2)
 
-        const abuse = res.body.data.find(a => a.reason === 'will mute this')
+        const abuse = body.data.find(a => a.reason === 'will mute this')
         expect(abuse).to.be.undefined
       }
 
       {
-        await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock)
+        await servers[0].blocklist.removeFromServerBlocklist({ server: serverToBlock })
 
-        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
-        expect(res.body.total).to.equal(3)
+        const body = await commands[0].getAdminList()
+        expect(body.total).to.equal(3)
       }
     })
 
     it('Should keep the video abuse when deleting the video', async function () {
       this.timeout(10000)
 
-      await removeVideo(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid)
+      await servers[1].videos.remove({ id: abuseServer2.video.uuid })
 
       await waitJobs(servers)
 
-      const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken })
-      expect(res.body.total).to.equal(2, "wrong number of videos returned")
-      expect(res.body.data).to.have.lengthOf(2, "wrong number of videos returned")
+      const body = await commands[1].getAdminList()
+      expect(body.total).to.equal(2, "wrong number of videos returned")
+      expect(body.data).to.have.lengthOf(2, "wrong number of videos returned")
 
-      const abuse: AdminAbuse = res.body.data[0]
+      const abuse = body.data[0]
       expect(abuse.id).to.equal(abuseServer2.id, "wrong origin server id for first video")
       expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id")
       expect(abuse.video.channel).to.exist
@@ -295,39 +272,36 @@ describe('Test abuses', function () {
 
       // register a second user to have two reporters/reportees
       const user = { username: 'user2', password: 'password' }
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, ...user })
-      const userAccessToken = await userLogin(servers[0], user)
+      await servers[0].users.create({ ...user })
+      const userAccessToken = await servers[0].login.getAccessToken(user)
 
       // upload a third video via this user
-      const video3Attributes = {
+      const attributes = {
         name: 'my second super name for server 1',
         description: 'my second super description for server 1'
       }
-      await uploadVideo(servers[0].url, userAccessToken, video3Attributes)
-
-      const res1 = await getVideosList(servers[0].url)
-      const videos = res1.body.data
-      const video3 = videos.find(video => video.name === 'my second super name for server 1')
+      const { id } = await servers[0].videos.upload({ token: userAccessToken, attributes })
+      const video3Id = id
 
       // resume with the test
       const reason3 = 'my super bad reason 3'
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: video3.id, reason: reason3 })
+      await commands[0].report({ videoId: video3Id, reason: reason3 })
 
       const reason4 = 'my super bad reason 4'
-      await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: servers[0].video.id, reason: reason4 })
+      await commands[0].report({ token: userAccessToken, videoId: servers[0].store.videoCreated.id, reason: reason4 })
 
       {
-        const res2 = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
-        const abuses = res2.body.data as AdminAbuse[]
+        const body = await commands[0].getAdminList()
+        const abuses = body.data
 
-        const abuseVideo3 = res2.body.data.find(a => a.video.id === video3.id)
+        const abuseVideo3 = body.data.find(a => a.video.id === video3Id)
         expect(abuseVideo3).to.not.be.undefined
         expect(abuseVideo3.video.countReports).to.equal(1, "wrong reports count for video 3")
         expect(abuseVideo3.video.nthReport).to.equal(1, "wrong report position in report list for video 3")
         expect(abuseVideo3.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse")
         expect(abuseVideo3.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse")
 
-        const abuseServer1 = abuses.find(a => a.video.id === servers[0].video.id)
+        const abuseServer1 = abuses.find(a => a.video.id === servers[0].store.videoCreated.id)
         expect(abuseServer1.countReportsForReportee).to.equal(3, "wrong reports count for reporter on video 1 abuse")
       }
     })
@@ -337,20 +311,18 @@ describe('Test abuses', function () {
 
       const reason5 = 'my super bad reason 5'
       const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
-      const createdAbuse = (await reportAbuse({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        videoId: servers[0].video.id,
+      const createRes = await commands[0].report({
+        videoId: servers[0].store.videoCreated.id,
         reason: reason5,
         predefinedReasons: predefinedReasons5,
         startAt: 1,
         endAt: 5
-      })).body.abuse
+      })
 
-      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+      const body = await commands[0].getAdminList()
 
       {
-        const abuse = (res.body.data as AdminAbuse[]).find(a => a.id === createdAbuse.id)
+        const abuse = body.data.find(a => a.id === createRes.abuse.id)
         expect(abuse.reason).to.equals(reason5)
         expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
         expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
@@ -361,37 +333,30 @@ describe('Test abuses', function () {
     it('Should delete the video abuse', async function () {
       this.timeout(10000)
 
-      await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id)
+      await commands[1].delete({ abuseId: abuseServer2.id })
 
       await waitJobs(servers)
 
       {
-        const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken })
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data.length).to.equal(1)
-        expect(res.body.data[0].id).to.not.equal(abuseServer2.id)
+        const body = await commands[1].getAdminList()
+        expect(body.total).to.equal(1)
+        expect(body.data.length).to.equal(1)
+        expect(body.data[0].id).to.not.equal(abuseServer2.id)
       }
 
       {
-        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
-        expect(res.body.total).to.equal(6)
+        const body = await commands[0].getAdminList()
+        expect(body.total).to.equal(6)
       }
     })
 
     it('Should list and filter video abuses', async function () {
       this.timeout(10000)
 
-      async function list (query: Omit<Parameters<typeof getAdminAbusesList>[0], 'url' | 'token'>) {
-        const options = {
-          url: servers[0].url,
-          token: servers[0].accessToken
-        }
-
-        Object.assign(options, query)
+      async function list (query: Parameters<AbusesCommand['getAdminList']>[0]) {
+        const body = await commands[0].getAdminList(query)
 
-        const res = await getAdminAbusesList(options)
-
-        return res.body.data as AdminAbuse[]
+        return body.data
       }
 
       expect(await list({ id: 56 })).to.have.lengthOf(0)
@@ -424,24 +389,24 @@ describe('Test abuses', function () {
 
   describe('Comment abuses', function () {
 
-    async function getComment (url: string, videoIdArg: number | string) {
+    async function getComment (server: PeerTubeServer, videoIdArg: number | string) {
       const videoId = typeof videoIdArg === 'string'
-        ? await getVideoIdFromUUID(url, videoIdArg)
+        ? await server.videos.getId({ uuid: videoIdArg })
         : videoIdArg
 
-      const res = await getVideoCommentThreads(url, videoId, 0, 5)
+      const { data } = await server.comments.listThreads({ videoId })
 
-      return res.body.data[0] as VideoComment
+      return data[0]
     }
 
     before(async function () {
       this.timeout(50000)
 
-      servers[0].video = await uploadVideoAndGetId({ server: servers[0], videoName: 'server 1' })
-      servers[1].video = await uploadVideoAndGetId({ server: servers[1], videoName: 'server 2' })
+      servers[0].store.videoCreated = await servers[0].videos.quickUpload({ name: 'server 1' })
+      servers[1].store.videoCreated = await servers[1].videos.quickUpload({ name: 'server 2' })
 
-      await addVideoCommentThread(servers[0].url, servers[0].accessToken, servers[0].video.id, 'comment server 1')
-      await addVideoCommentThread(servers[1].url, servers[1].accessToken, servers[1].video.id, 'comment server 2')
+      await servers[0].comments.createThread({ videoId: servers[0].store.videoCreated.id, text: 'comment server 1' })
+      await servers[1].comments.createThread({ videoId: servers[1].store.videoCreated.id, text: 'comment server 2' })
 
       await waitJobs(servers)
     })
@@ -449,23 +414,23 @@ describe('Test abuses', function () {
     it('Should report abuse on a comment', async function () {
       this.timeout(15000)
 
-      const comment = await getComment(servers[0].url, servers[0].video.id)
+      const comment = await getComment(servers[0], servers[0].store.videoCreated.id)
 
       const reason = 'it is a bad comment'
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason })
+      await commands[0].report({ commentId: comment.id, reason })
 
       await waitJobs(servers)
     })
 
     it('Should have 1 comment abuse on server 1 and 0 on server 2', async function () {
       {
-        const comment = await getComment(servers[0].url, servers[0].video.id)
-        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
+        const comment = await getComment(servers[0], servers[0].store.videoCreated.id)
+        const body = await commands[0].getAdminList({ filter: 'comment' })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(body.total).to.equal(1)
+        expect(body.data).to.have.lengthOf(1)
 
-        const abuse: AdminAbuse = res.body.data[0]
+        const abuse = body.data[0]
         expect(abuse.reason).to.equal('it is a bad comment')
 
         expect(abuse.reporterAccount.name).to.equal('root')
@@ -477,98 +442,102 @@ describe('Test abuses', function () {
         expect(abuse.comment.id).to.equal(comment.id)
         expect(abuse.comment.text).to.equal(comment.text)
         expect(abuse.comment.video.name).to.equal('server 1')
-        expect(abuse.comment.video.id).to.equal(servers[0].video.id)
-        expect(abuse.comment.video.uuid).to.equal(servers[0].video.uuid)
+        expect(abuse.comment.video.id).to.equal(servers[0].store.videoCreated.id)
+        expect(abuse.comment.video.uuid).to.equal(servers[0].store.videoCreated.uuid)
 
         expect(abuse.countReportsForReporter).to.equal(5)
         expect(abuse.countReportsForReportee).to.equal(5)
       }
 
       {
-        const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data.length).to.equal(0)
+        const body = await commands[1].getAdminList({ filter: 'comment' })
+        expect(body.total).to.equal(0)
+        expect(body.data.length).to.equal(0)
       }
     })
 
     it('Should report abuse on a remote comment', async function () {
       this.timeout(10000)
 
-      const comment = await getComment(servers[0].url, servers[1].video.uuid)
+      const comment = await getComment(servers[0], servers[1].store.videoCreated.uuid)
 
       const reason = 'it is a really bad comment'
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason })
+      await commands[0].report({ commentId: comment.id, reason })
 
       await waitJobs(servers)
     })
 
     it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () {
-      const commentServer2 = await getComment(servers[0].url, servers[1].video.id)
+      const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.id)
 
-      const res1 = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
-      expect(res1.body.total).to.equal(2)
-      expect(res1.body.data.length).to.equal(2)
+      {
+        const body = await commands[0].getAdminList({ filter: 'comment' })
+        expect(body.total).to.equal(2)
+        expect(body.data.length).to.equal(2)
 
-      const abuse: AdminAbuse = res1.body.data[0]
-      expect(abuse.reason).to.equal('it is a bad comment')
-      expect(abuse.countReportsForReporter).to.equal(6)
-      expect(abuse.countReportsForReportee).to.equal(5)
+        const abuse = body.data[0]
+        expect(abuse.reason).to.equal('it is a bad comment')
+        expect(abuse.countReportsForReporter).to.equal(6)
+        expect(abuse.countReportsForReportee).to.equal(5)
 
-      const abuse2: AdminAbuse = res1.body.data[1]
+        const abuse2 = body.data[1]
 
-      expect(abuse2.reason).to.equal('it is a really bad comment')
+        expect(abuse2.reason).to.equal('it is a really bad comment')
 
-      expect(abuse2.reporterAccount.name).to.equal('root')
-      expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
+        expect(abuse2.reporterAccount.name).to.equal('root')
+        expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
 
-      expect(abuse2.video).to.be.null
+        expect(abuse2.video).to.be.null
 
-      expect(abuse2.comment.deleted).to.be.false
-      expect(abuse2.comment.id).to.equal(commentServer2.id)
-      expect(abuse2.comment.text).to.equal(commentServer2.text)
-      expect(abuse2.comment.video.name).to.equal('server 2')
-      expect(abuse2.comment.video.uuid).to.equal(servers[1].video.uuid)
+        expect(abuse2.comment.deleted).to.be.false
+        expect(abuse2.comment.id).to.equal(commentServer2.id)
+        expect(abuse2.comment.text).to.equal(commentServer2.text)
+        expect(abuse2.comment.video.name).to.equal('server 2')
+        expect(abuse2.comment.video.uuid).to.equal(servers[1].store.videoCreated.uuid)
 
-      expect(abuse2.state.id).to.equal(AbuseState.PENDING)
-      expect(abuse2.state.label).to.equal('Pending')
+        expect(abuse2.state.id).to.equal(AbuseState.PENDING)
+        expect(abuse2.state.label).to.equal('Pending')
 
-      expect(abuse2.moderationComment).to.be.null
+        expect(abuse2.moderationComment).to.be.null
 
-      expect(abuse2.countReportsForReporter).to.equal(6)
-      expect(abuse2.countReportsForReportee).to.equal(2)
+        expect(abuse2.countReportsForReporter).to.equal(6)
+        expect(abuse2.countReportsForReportee).to.equal(2)
+      }
 
-      const res2 = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
-      expect(res2.body.total).to.equal(1)
-      expect(res2.body.data.length).to.equal(1)
+      {
+        const body = await commands[1].getAdminList({ filter: 'comment' })
+        expect(body.total).to.equal(1)
+        expect(body.data.length).to.equal(1)
 
-      abuseServer2 = res2.body.data[0]
-      expect(abuseServer2.reason).to.equal('it is a really bad comment')
-      expect(abuseServer2.reporterAccount.name).to.equal('root')
-      expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
+        abuseServer2 = body.data[0]
+        expect(abuseServer2.reason).to.equal('it is a really bad comment')
+        expect(abuseServer2.reporterAccount.name).to.equal('root')
+        expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
 
-      expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
-      expect(abuseServer2.state.label).to.equal('Pending')
+        expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
+        expect(abuseServer2.state.label).to.equal('Pending')
 
-      expect(abuseServer2.moderationComment).to.be.null
+        expect(abuseServer2.moderationComment).to.be.null
 
-      expect(abuseServer2.countReportsForReporter).to.equal(1)
-      expect(abuseServer2.countReportsForReportee).to.equal(1)
+        expect(abuseServer2.countReportsForReporter).to.equal(1)
+        expect(abuseServer2.countReportsForReportee).to.equal(1)
+      }
     })
 
     it('Should keep the comment abuse when deleting the comment', async function () {
       this.timeout(10000)
 
-      const commentServer2 = await getComment(servers[0].url, servers[1].video.id)
+      const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.id)
 
-      await deleteVideoComment(servers[0].url, servers[0].accessToken, servers[1].video.uuid, commentServer2.id)
+      await servers[0].comments.delete({ videoId: servers[1].store.videoCreated.uuid, commentId: commentServer2.id })
 
       await waitJobs(servers)
 
-      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.have.lengthOf(2)
+      const body = await commands[0].getAdminList({ filter: 'comment' })
+      expect(body.total).to.equal(2)
+      expect(body.data).to.have.lengthOf(2)
 
-      const abuse = (res.body.data as AdminAbuse[]).find(a => a.comment?.id === commentServer2.id)
+      const abuse = body.data.find(a => a.comment?.id === commentServer2.id)
       expect(abuse).to.not.be.undefined
 
       expect(abuse.comment.text).to.be.empty
@@ -579,72 +548,60 @@ describe('Test abuses', function () {
     it('Should delete the comment abuse', async function () {
       this.timeout(10000)
 
-      await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id)
+      await commands[1].delete({ abuseId: abuseServer2.id })
 
       await waitJobs(servers)
 
       {
-        const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data.length).to.equal(0)
+        const body = await commands[1].getAdminList({ filter: 'comment' })
+        expect(body.total).to.equal(0)
+        expect(body.data.length).to.equal(0)
       }
 
       {
-        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
-        expect(res.body.total).to.equal(2)
+        const body = await commands[0].getAdminList({ filter: 'comment' })
+        expect(body.total).to.equal(2)
       }
     })
 
     it('Should list and filter video abuses', async function () {
       {
-        const res = await getAdminAbusesList({
-          url: servers[0].url,
-          token: servers[0].accessToken,
-          filter: 'comment',
-          searchReportee: 'foo'
-        })
-        expect(res.body.total).to.equal(0)
+        const body = await commands[0].getAdminList({ filter: 'comment', searchReportee: 'foo' })
+        expect(body.total).to.equal(0)
       }
 
       {
-        const res = await getAdminAbusesList({
-          url: servers[0].url,
-          token: servers[0].accessToken,
-          filter: 'comment',
-          searchReportee: 'ot'
-        })
-        expect(res.body.total).to.equal(2)
+        const body = await commands[0].getAdminList({ filter: 'comment', searchReportee: 'ot' })
+        expect(body.total).to.equal(2)
       }
 
       {
-        const baseParams = { url: servers[0].url, token: servers[0].accessToken, filter: 'comment' as AbuseFilter, start: 1, count: 1 }
-
-        const res1 = await getAdminAbusesList(immutableAssign(baseParams, { sort: 'createdAt' }))
-        expect(res1.body.data).to.have.lengthOf(1)
-        expect(res1.body.data[0].comment.text).to.be.empty
+        const body = await commands[0].getAdminList({ filter: 'comment', start: 1, count: 1, sort: 'createdAt' })
+        expect(body.data).to.have.lengthOf(1)
+        expect(body.data[0].comment.text).to.be.empty
+      }
 
-        const res2 = await getAdminAbusesList(immutableAssign(baseParams, { sort: '-createdAt' }))
-        expect(res2.body.data).to.have.lengthOf(1)
-        expect(res2.body.data[0].comment.text).to.equal('comment server 1')
+      {
+        const body = await commands[0].getAdminList({ filter: 'comment', start: 1, count: 1, sort: '-createdAt' })
+        expect(body.data).to.have.lengthOf(1)
+        expect(body.data[0].comment.text).to.equal('comment server 1')
       }
     })
   })
 
   describe('Account abuses', function () {
 
-    async function getAccountFromServer (url: string, name: string, server: ServerInfo) {
-      const res = await getAccount(url, name + '@' + server.host)
-
-      return res.body as Account
+    function getAccountFromServer (server: PeerTubeServer, targetName: string, targetServer: PeerTubeServer) {
+      return server.accounts.get({ accountName: targetName + '@' + targetServer.host })
     }
 
     before(async function () {
       this.timeout(50000)
 
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: 'user_1', password: 'donald' })
+      await servers[0].users.create({ username: 'user_1', password: 'donald' })
 
-      const token = await generateUserAccessToken(servers[1], 'user_2')
-      await uploadVideo(servers[1].url, token, { name: 'super video' })
+      const token = await servers[1].users.generateUserAndToken('user_2')
+      await servers[1].videos.upload({ token, attributes: { name: 'super video' } })
 
       await waitJobs(servers)
     })
@@ -652,22 +609,22 @@ describe('Test abuses', function () {
     it('Should report abuse on an account', async function () {
       this.timeout(15000)
 
-      const account = await getAccountFromServer(servers[0].url, 'user_1', servers[0])
+      const account = await getAccountFromServer(servers[0], 'user_1', servers[0])
 
       const reason = 'it is a bad account'
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId: account.id, reason })
+      await commands[0].report({ accountId: account.id, reason })
 
       await waitJobs(servers)
     })
 
     it('Should have 1 account abuse on server 1 and 0 on server 2', async function () {
       {
-        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
+        const body = await commands[0].getAdminList({ filter: 'account' })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(body.total).to.equal(1)
+        expect(body.data).to.have.lengthOf(1)
 
-        const abuse: AdminAbuse = res.body.data[0]
+        const abuse = body.data[0]
         expect(abuse.reason).to.equal('it is a bad account')
 
         expect(abuse.reporterAccount.name).to.equal('root')
@@ -681,96 +638,100 @@ describe('Test abuses', function () {
       }
 
       {
-        const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data.length).to.equal(0)
+        const body = await commands[1].getAdminList({ filter: 'comment' })
+        expect(body.total).to.equal(0)
+        expect(body.data.length).to.equal(0)
       }
     })
 
     it('Should report abuse on a remote account', async function () {
       this.timeout(10000)
 
-      const account = await getAccountFromServer(servers[0].url, 'user_2', servers[1])
+      const account = await getAccountFromServer(servers[0], 'user_2', servers[1])
 
       const reason = 'it is a really bad account'
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId: account.id, reason })
+      await commands[0].report({ accountId: account.id, reason })
 
       await waitJobs(servers)
     })
 
     it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () {
-      const res1 = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
-      expect(res1.body.total).to.equal(2)
-      expect(res1.body.data.length).to.equal(2)
+      {
+        const body = await commands[0].getAdminList({ filter: 'account' })
+        expect(body.total).to.equal(2)
+        expect(body.data.length).to.equal(2)
 
-      const abuse: AdminAbuse = res1.body.data[0]
-      expect(abuse.reason).to.equal('it is a bad account')
+        const abuse: AdminAbuse = body.data[0]
+        expect(abuse.reason).to.equal('it is a bad account')
 
-      const abuse2: AdminAbuse = res1.body.data[1]
-      expect(abuse2.reason).to.equal('it is a really bad account')
+        const abuse2: AdminAbuse = body.data[1]
+        expect(abuse2.reason).to.equal('it is a really bad account')
 
-      expect(abuse2.reporterAccount.name).to.equal('root')
-      expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
+        expect(abuse2.reporterAccount.name).to.equal('root')
+        expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
 
-      expect(abuse2.video).to.be.null
-      expect(abuse2.comment).to.be.null
+        expect(abuse2.video).to.be.null
+        expect(abuse2.comment).to.be.null
 
-      expect(abuse2.state.id).to.equal(AbuseState.PENDING)
-      expect(abuse2.state.label).to.equal('Pending')
+        expect(abuse2.state.id).to.equal(AbuseState.PENDING)
+        expect(abuse2.state.label).to.equal('Pending')
 
-      expect(abuse2.moderationComment).to.be.null
+        expect(abuse2.moderationComment).to.be.null
+      }
 
-      const res2 = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' })
-      expect(res2.body.total).to.equal(1)
-      expect(res2.body.data.length).to.equal(1)
+      {
+        const body = await commands[1].getAdminList({ filter: 'account' })
+        expect(body.total).to.equal(1)
+        expect(body.data.length).to.equal(1)
 
-      abuseServer2 = res2.body.data[0]
+        abuseServer2 = body.data[0]
 
-      expect(abuseServer2.reason).to.equal('it is a really bad account')
+        expect(abuseServer2.reason).to.equal('it is a really bad account')
 
-      expect(abuseServer2.reporterAccount.name).to.equal('root')
-      expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
+        expect(abuseServer2.reporterAccount.name).to.equal('root')
+        expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
 
-      expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
-      expect(abuseServer2.state.label).to.equal('Pending')
+        expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
+        expect(abuseServer2.state.label).to.equal('Pending')
 
-      expect(abuseServer2.moderationComment).to.be.null
+        expect(abuseServer2.moderationComment).to.be.null
+      }
     })
 
     it('Should keep the account abuse when deleting the account', async function () {
       this.timeout(10000)
 
-      const account = await getAccountFromServer(servers[1].url, 'user_2', servers[1])
-      await removeUser(servers[1].url, account.userId, servers[1].accessToken)
+      const account = await getAccountFromServer(servers[1], 'user_2', servers[1])
+      await servers[1].users.remove({ userId: account.userId })
 
       await waitJobs(servers)
 
-      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.have.lengthOf(2)
+      const body = await commands[0].getAdminList({ filter: 'account' })
+      expect(body.total).to.equal(2)
+      expect(body.data).to.have.lengthOf(2)
 
-      const abuse = (res.body.data as AdminAbuse[]).find(a => a.reason === 'it is a really bad account')
+      const abuse = body.data.find(a => a.reason === 'it is a really bad account')
       expect(abuse).to.not.be.undefined
     })
 
     it('Should delete the account abuse', async function () {
       this.timeout(10000)
 
-      await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id)
+      await commands[1].delete({ abuseId: abuseServer2.id })
 
       await waitJobs(servers)
 
       {
-        const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' })
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data.length).to.equal(0)
+        const body = await commands[1].getAdminList({ filter: 'account' })
+        expect(body.total).to.equal(0)
+        expect(body.data.length).to.equal(0)
       }
 
       {
-        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
-        expect(res.body.total).to.equal(2)
+        const body = await commands[0].getAdminList({ filter: 'account' })
+        expect(body.total).to.equal(2)
 
-        abuseServer1 = res.body.data[0]
+        abuseServer1 = body.data[0]
       }
     })
   })
@@ -778,20 +739,18 @@ describe('Test abuses', function () {
   describe('Common actions on abuses', function () {
 
     it('Should update the state of an abuse', async function () {
-      const body = { state: AbuseState.REJECTED }
-      await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body)
+      await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.REJECTED } })
 
-      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id })
-      expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED)
+      const body = await commands[0].getAdminList({ id: abuseServer1.id })
+      expect(body.data[0].state.id).to.equal(AbuseState.REJECTED)
     })
 
     it('Should add a moderation comment', async function () {
-      const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' }
-      await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body)
+      await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.ACCEPTED, moderationComment: 'Valid' } })
 
-      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id })
-      expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
-      expect(res.body.data[0].moderationComment).to.equal('It is valid')
+      const body = await commands[0].getAdminList({ id: abuseServer1.id })
+      expect(body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
+      expect(body.data[0].moderationComment).to.equal('Valid')
     })
   })
 
@@ -800,20 +759,20 @@ describe('Test abuses', function () {
     let userAccessToken: string
 
     before(async function () {
-      userAccessToken = await generateUserAccessToken(servers[0], 'user_42')
+      userAccessToken = await servers[0].users.generateUserAndToken('user_42')
 
-      await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: servers[0].video.id, reason: 'user reason 1' })
+      await commands[0].report({ token: userAccessToken, videoId: servers[0].store.videoCreated.id, reason: 'user reason 1' })
 
-      const videoId = await getVideoIdFromUUID(servers[0].url, servers[1].video.uuid)
-      await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId, reason: 'user reason 2' })
+      const videoId = await servers[0].videos.getId({ uuid: servers[1].store.videoCreated.uuid })
+      await commands[0].report({ token: userAccessToken, videoId, reason: 'user reason 2' })
     })
 
     it('Should correctly list my abuses', async function () {
       {
-        const res = await getUserAbusesList({ url: servers[0].url, token: userAccessToken, start: 0, count: 5, sort: 'createdAt' })
-        expect(res.body.total).to.equal(2)
+        const body = await commands[0].getUserList({ token: userAccessToken, start: 0, count: 5, sort: 'createdAt' })
+        expect(body.total).to.equal(2)
 
-        const abuses: UserAbuse[] = res.body.data
+        const abuses = body.data
         expect(abuses[0].reason).to.equal('user reason 1')
         expect(abuses[1].reason).to.equal('user reason 2')
 
@@ -821,95 +780,77 @@ describe('Test abuses', function () {
       }
 
       {
-        const res = await getUserAbusesList({ url: servers[0].url, token: userAccessToken, start: 1, count: 1, sort: 'createdAt' })
-        expect(res.body.total).to.equal(2)
+        const body = await commands[0].getUserList({ token: userAccessToken, start: 1, count: 1, sort: 'createdAt' })
+        expect(body.total).to.equal(2)
 
-        const abuses: UserAbuse[] = res.body.data
+        const abuses: UserAbuse[] = body.data
         expect(abuses[0].reason).to.equal('user reason 2')
       }
 
       {
-        const res = await getUserAbusesList({ url: servers[0].url, token: userAccessToken, start: 1, count: 1, sort: '-createdAt' })
-        expect(res.body.total).to.equal(2)
+        const body = await commands[0].getUserList({ token: userAccessToken, start: 1, count: 1, sort: '-createdAt' })
+        expect(body.total).to.equal(2)
 
-        const abuses: UserAbuse[] = res.body.data
+        const abuses: UserAbuse[] = body.data
         expect(abuses[0].reason).to.equal('user reason 1')
       }
     })
 
     it('Should correctly filter my abuses by id', async function () {
-      const res = await getUserAbusesList({ url: servers[0].url, token: userAccessToken, id: abuseId1 })
+      const body = await commands[0].getUserList({ token: userAccessToken, id: abuseId1 })
+      expect(body.total).to.equal(1)
 
-      expect(res.body.total).to.equal(1)
-
-      const abuses: UserAbuse[] = res.body.data
+      const abuses: UserAbuse[] = body.data
       expect(abuses[0].reason).to.equal('user reason 1')
     })
 
     it('Should correctly filter my abuses by search', async function () {
-      const res = await getUserAbusesList({
-        url: servers[0].url,
-        token: userAccessToken,
-        search: 'server 2'
-      })
-
-      expect(res.body.total).to.equal(1)
+      const body = await commands[0].getUserList({ token: userAccessToken, search: 'server 2' })
+      expect(body.total).to.equal(1)
 
-      const abuses: UserAbuse[] = res.body.data
+      const abuses: UserAbuse[] = body.data
       expect(abuses[0].reason).to.equal('user reason 2')
     })
 
     it('Should correctly filter my abuses by state', async function () {
-      const body = { state: AbuseState.REJECTED }
-      await updateAbuse(servers[0].url, servers[0].accessToken, abuseId1, body)
+      await commands[0].update({ abuseId: abuseId1, body: { state: AbuseState.REJECTED } })
 
-      const res = await getUserAbusesList({
-        url: servers[0].url,
-        token: userAccessToken,
-        state: AbuseState.REJECTED
-      })
-
-      expect(res.body.total).to.equal(1)
+      const body = await commands[0].getUserList({ token: userAccessToken, state: AbuseState.REJECTED })
+      expect(body.total).to.equal(1)
 
-      const abuses: UserAbuse[] = res.body.data
+      const abuses: UserAbuse[] = body.data
       expect(abuses[0].reason).to.equal('user reason 1')
     })
   })
 
   describe('Abuse messages', async function () {
     let abuseId: number
-    let userAccessToken: string
+    let userToken: string
     let abuseMessageUserId: number
     let abuseMessageModerationId: number
 
     before(async function () {
-      userAccessToken = await generateUserAccessToken(servers[0], 'user_43')
+      userToken = await servers[0].users.generateUserAndToken('user_43')
 
-      const res = await reportAbuse({
-        url: servers[0].url,
-        token: userAccessToken,
-        videoId: servers[0].video.id,
-        reason: 'user 43 reason 1'
-      })
-
-      abuseId = res.body.abuse.id
+      const body = await commands[0].report({ token: userToken, videoId: servers[0].store.videoCreated.id, reason: 'user 43 reason 1' })
+      abuseId = body.abuse.id
     })
 
     it('Should create some messages on the abuse', async function () {
-      await addAbuseMessage(servers[0].url, userAccessToken, abuseId, 'message 1')
-      await addAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, 'message 2')
-      await addAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, 'message 3')
-      await addAbuseMessage(servers[0].url, userAccessToken, abuseId, 'message 4')
+      await commands[0].addMessage({ token: userToken, abuseId, message: 'message 1' })
+      await commands[0].addMessage({ abuseId, message: 'message 2' })
+      await commands[0].addMessage({ abuseId, message: 'message 3' })
+      await commands[0].addMessage({ token: userToken, abuseId, message: 'message 4' })
     })
 
     it('Should have the correct messages count when listing abuses', async function () {
       const results = await Promise.all([
-        getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, start: 0, count: 50 }),
-        getUserAbusesList({ url: servers[0].url, token: userAccessToken, start: 0, count: 50 })
+        commands[0].getAdminList({ start: 0, count: 50 }),
+        commands[0].getUserList({ token: userToken, start: 0, count: 50 })
       ])
 
-      for (const res of results) {
-        const abuses: AdminAbuse[] = res.body.data
+      for (const body of results) {
+        const abuses = body.data
         const abuse = abuses.find(a => a.id === abuseId)
         expect(abuse.countMessages).to.equal(4)
       }
@@ -917,14 +858,14 @@ describe('Test abuses', function () {
 
     it('Should correctly list messages of this abuse', async function () {
       const results = await Promise.all([
-        listAbuseMessages(servers[0].url, servers[0].accessToken, abuseId),
-        listAbuseMessages(servers[0].url, userAccessToken, abuseId)
+        commands[0].listMessages({ abuseId }),
+        commands[0].listMessages({ token: userToken, abuseId })
       ])
 
-      for (const res of results) {
-        expect(res.body.total).to.equal(4)
+      for (const body of results) {
+        expect(body.total).to.equal(4)
 
-        const abuseMessages: AbuseMessage[] = res.body.data
+        const abuseMessages: AbuseMessage[] = body.data
 
         expect(abuseMessages[0].message).to.equal('message 1')
         expect(abuseMessages[0].byModerator).to.be.false
@@ -948,19 +889,18 @@ describe('Test abuses', function () {
     })
 
     it('Should delete messages', async function () {
-      await deleteAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, abuseMessageModerationId)
-      await deleteAbuseMessage(servers[0].url, userAccessToken, abuseId, abuseMessageUserId)
+      await commands[0].deleteMessage({ abuseId, messageId: abuseMessageModerationId })
+      await commands[0].deleteMessage({ token: userToken, abuseId, messageId: abuseMessageUserId })
 
       const results = await Promise.all([
-        listAbuseMessages(servers[0].url, servers[0].accessToken, abuseId),
-        listAbuseMessages(servers[0].url, userAccessToken, abuseId)
+        commands[0].listMessages({ abuseId }),
+        commands[0].listMessages({ token: userToken, abuseId })
       ])
 
-      for (const res of results) {
-        expect(res.body.total).to.equal(2)
-
-        const abuseMessages: AbuseMessage[] = res.body.data
+      for (const body of results) {
+        expect(body.total).to.equal(2)
 
+        const abuseMessages: AbuseMessage[] = body.data
         expect(abuseMessages[0].message).to.equal('message 2')
         expect(abuseMessages[1].message).to.equal('message 4')
       }
index 4fb3c95f2ef18eca8326f609b0ef81862ab2f2c2..75b15c298729d793184f3b6e665edc157f3d3c1e 100644 (file)
@@ -2,47 +2,22 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { getUserNotifications, markAsReadAllNotifications } from '@shared/extra-utils/users/user-notifications'
-import { addUserSubscription, removeUserSubscription } from '@shared/extra-utils/users/user-subscriptions'
-import { UserNotification, UserNotificationType } from '@shared/models'
-import {
-  cleanupTests,
-  createUser,
-  doubleFollow,
-  flushAndRunMultipleServers,
-  ServerInfo,
-  uploadVideo,
-  userLogin
-} from '../../../../shared/extra-utils/index'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import {
-  addAccountToAccountBlocklist,
-  addAccountToServerBlocklist,
-  addServerToAccountBlocklist,
-  addServerToServerBlocklist,
-  removeAccountFromAccountBlocklist,
-  removeAccountFromServerBlocklist,
-  removeServerFromAccountBlocklist
-} from '../../../../shared/extra-utils/users/blocklist'
-import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
-import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
+import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
+import { UserNotificationType } from '@shared/models'
 
 const expect = chai.expect
 
-async function checkNotifications (url: string, token: string, expected: UserNotificationType[]) {
-  const res = await getUserNotifications(url, token, 0, 10, true)
-
-  const notifications: UserNotification[] = res.body.data
-
-  expect(notifications).to.have.lengthOf(expected.length)
+async function checkNotifications (server: PeerTubeServer, token: string, expected: UserNotificationType[]) {
+  const { data } = await server.notifications.list({ token, start: 0, count: 10, unread: true })
+  expect(data).to.have.lengthOf(expected.length)
 
   for (const type of expected) {
-    expect(notifications.find(n => n.type === type)).to.exist
+    expect(data.find(n => n.type === type)).to.exist
   }
 }
 
 describe('Test blocklist', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let videoUUID: string
 
   let userToken1: string
@@ -51,30 +26,34 @@ describe('Test blocklist', function () {
 
   async function resetState () {
     try {
-      await removeUserSubscription(servers[1].url, remoteUserToken, 'user1_channel@' + servers[0].host)
-      await removeUserSubscription(servers[1].url, remoteUserToken, 'user2_channel@' + servers[0].host)
+      await servers[1].subscriptions.remove({ token: remoteUserToken, uri: 'user1_channel@' + servers[0].host })
+      await servers[1].subscriptions.remove({ token: remoteUserToken, uri: 'user2_channel@' + servers[0].host })
     } catch {}
 
     await waitJobs(servers)
 
-    await markAsReadAllNotifications(servers[0].url, userToken1)
-    await markAsReadAllNotifications(servers[0].url, userToken2)
+    await servers[0].notifications.markAsReadAll({ token: userToken1 })
+    await servers[0].notifications.markAsReadAll({ token: userToken2 })
 
     {
-      const res = await uploadVideo(servers[0].url, userToken1, { name: 'video' })
-      videoUUID = res.body.video.uuid
+      const { uuid } = await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video' } })
+      videoUUID = uuid
 
       await waitJobs(servers)
     }
 
     {
-      await addVideoCommentThread(servers[1].url, remoteUserToken, videoUUID, '@user2@' + servers[0].host + ' hello')
+      await servers[1].comments.createThread({
+        token: remoteUserToken,
+        videoId: videoUUID,
+        text: '@user2@' + servers[0].host + ' hello'
+      })
     }
 
     {
 
-      await addUserSubscription(servers[1].url, remoteUserToken, 'user1_channel@' + servers[0].host)
-      await addUserSubscription(servers[1].url, remoteUserToken, 'user2_channel@' + servers[0].host)
+      await servers[1].subscriptions.add({ token: remoteUserToken, targetUri: 'user1_channel@' + servers[0].host })
+      await servers[1].subscriptions.add({ token: remoteUserToken, targetUri: 'user2_channel@' + servers[0].host })
     }
 
     await waitJobs(servers)
@@ -83,36 +62,34 @@ describe('Test blocklist', function () {
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
 
     {
       const user = { username: 'user1', password: 'password' }
-      await createUser({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
+      await servers[0].users.create({
         username: user.username,
         password: user.password,
         videoQuota: -1,
         videoQuotaDaily: -1
       })
 
-      userToken1 = await userLogin(servers[0], user)
-      await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' })
+      userToken1 = await servers[0].login.getAccessToken(user)
+      await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video user 1' } })
     }
 
     {
       const user = { username: 'user2', password: 'password' }
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
+      await servers[0].users.create({ username: user.username, password: user.password })
 
-      userToken2 = await userLogin(servers[0], user)
+      userToken2 = await servers[0].login.getAccessToken(user)
     }
 
     {
       const user = { username: 'user3', password: 'password' }
-      await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
+      await servers[1].users.create({ username: user.username, password: user.password })
 
-      remoteUserToken = await userLogin(servers[1], user)
+      remoteUserToken = await servers[1].login.getAccessToken(user)
     }
 
     await doubleFollow(servers[0], servers[1])
@@ -128,26 +105,26 @@ describe('Test blocklist', function () {
 
     it('Should have appropriate notifications', async function () {
       const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
-      await checkNotifications(servers[0].url, userToken1, notifs)
+      await checkNotifications(servers[0], userToken1, notifs)
     })
 
     it('Should block an account', async function () {
       this.timeout(10000)
 
-      await addAccountToAccountBlocklist(servers[0].url, userToken1, 'user3@' + servers[1].host)
+      await servers[0].blocklist.addToMyBlocklist({ token: userToken1, account: 'user3@' + servers[1].host })
       await waitJobs(servers)
     })
 
     it('Should not have notifications from this account', async function () {
-      await checkNotifications(servers[0].url, userToken1, [])
+      await checkNotifications(servers[0], userToken1, [])
     })
 
     it('Should have notifications of this account on user 2', async function () {
       const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
 
-      await checkNotifications(servers[0].url, userToken2, notifs)
+      await checkNotifications(servers[0], userToken2, notifs)
 
-      await removeAccountFromAccountBlocklist(servers[0].url, userToken1, 'user3@' + servers[1].host)
+      await servers[0].blocklist.removeFromMyBlocklist({ token: userToken1, account: 'user3@' + servers[1].host })
     })
   })
 
@@ -161,26 +138,26 @@ describe('Test blocklist', function () {
 
     it('Should have appropriate notifications', async function () {
       const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
-      await checkNotifications(servers[0].url, userToken1, notifs)
+      await checkNotifications(servers[0], userToken1, notifs)
     })
 
     it('Should block an account', async function () {
       this.timeout(10000)
 
-      await addServerToAccountBlocklist(servers[0].url, userToken1, servers[1].host)
+      await servers[0].blocklist.addToMyBlocklist({ token: userToken1, server: servers[1].host })
       await waitJobs(servers)
     })
 
     it('Should not have notifications from this account', async function () {
-      await checkNotifications(servers[0].url, userToken1, [])
+      await checkNotifications(servers[0], userToken1, [])
     })
 
     it('Should have notifications of this account on user 2', async function () {
       const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
 
-      await checkNotifications(servers[0].url, userToken2, notifs)
+      await checkNotifications(servers[0], userToken2, notifs)
 
-      await removeServerFromAccountBlocklist(servers[0].url, userToken1, servers[1].host)
+      await servers[0].blocklist.removeFromMyBlocklist({ token: userToken1, server: servers[1].host })
     })
   })
 
@@ -195,27 +172,27 @@ describe('Test blocklist', function () {
     it('Should have appropriate notifications', async function () {
       {
         const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
-        await checkNotifications(servers[0].url, userToken1, notifs)
+        await checkNotifications(servers[0], userToken1, notifs)
       }
 
       {
         const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
-        await checkNotifications(servers[0].url, userToken2, notifs)
+        await checkNotifications(servers[0], userToken2, notifs)
       }
     })
 
     it('Should block an account', async function () {
       this.timeout(10000)
 
-      await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'user3@' + servers[1].host)
+      await servers[0].blocklist.addToServerBlocklist({ account: 'user3@' + servers[1].host })
       await waitJobs(servers)
     })
 
     it('Should not have notifications from this account', async function () {
-      await checkNotifications(servers[0].url, userToken1, [])
-      await checkNotifications(servers[0].url, userToken2, [])
+      await checkNotifications(servers[0], userToken1, [])
+      await checkNotifications(servers[0], userToken2, [])
 
-      await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, 'user3@' + servers[1].host)
+      await servers[0].blocklist.removeFromServerBlocklist({ account: 'user3@' + servers[1].host })
     })
   })
 
@@ -230,25 +207,25 @@ describe('Test blocklist', function () {
     it('Should have appropriate notifications', async function () {
       {
         const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
-        await checkNotifications(servers[0].url, userToken1, notifs)
+        await checkNotifications(servers[0], userToken1, notifs)
       }
 
       {
         const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
-        await checkNotifications(servers[0].url, userToken2, notifs)
+        await checkNotifications(servers[0], userToken2, notifs)
       }
     })
 
     it('Should block an account', async function () {
       this.timeout(10000)
 
-      await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host)
+      await servers[0].blocklist.addToServerBlocklist({ server: servers[1].host })
       await waitJobs(servers)
     })
 
     it('Should not have notifications from this account', async function () {
-      await checkNotifications(servers[0].url, userToken1, [])
-      await checkNotifications(servers[0].url, userToken2, [])
+      await checkNotifications(servers[0], userToken1, [])
+      await checkNotifications(servers[0], userToken2, [])
     })
   })
 
index 793abbcb4209a1514d06efd03fc742ec140b62cb..089af8b159c0f45e198a8082ff4242c15277064c 100644 (file)
 import 'mocha'
 import * as chai from 'chai'
 import {
-  addAccountToAccountBlocklist,
-  addAccountToServerBlocklist,
-  addServerToAccountBlocklist,
-  addServerToServerBlocklist,
-  addVideoCommentReply,
-  addVideoCommentThread,
+  BlocklistCommand,
   cleanupTests,
-  createUser,
-  deleteVideoComment,
+  CommentsCommand,
+  createMultipleServers,
   doubleFollow,
-  findCommentId,
-  flushAndRunMultipleServers,
-  follow,
-  getAccountBlocklistByAccount,
-  getAccountBlocklistByServer,
-  getServerBlocklistByAccount,
-  getServerBlocklistByServer,
-  getUserNotifications,
-  getVideoCommentThreads,
-  getVideosList,
-  getVideosListWithToken,
-  getVideoThreadComments,
-  removeAccountFromAccountBlocklist,
-  removeAccountFromServerBlocklist,
-  removeServerFromAccountBlocklist,
-  removeServerFromServerBlocklist,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  unfollow,
-  uploadVideo,
-  userLogin,
   waitJobs
 } from '@shared/extra-utils'
-import {
-  AccountBlock,
-  ServerBlock,
-  UserNotification,
-  UserNotificationType,
-  Video,
-  VideoComment,
-  VideoCommentThreadTree
-} from '@shared/models'
+import { UserNotificationType } from '@shared/models'
 
 const expect = chai.expect
 
-async function checkAllVideos (url: string, token: string) {
+async function checkAllVideos (server: PeerTubeServer, token: string) {
   {
-    const res = await getVideosListWithToken(url, token)
-
-    expect(res.body.data).to.have.lengthOf(5)
+    const { data } = await server.videos.listWithToken({ token })
+    expect(data).to.have.lengthOf(5)
   }
 
   {
-    const res = await getVideosList(url)
-
-    expect(res.body.data).to.have.lengthOf(5)
+    const { data } = await server.videos.list()
+    expect(data).to.have.lengthOf(5)
   }
 }
 
-async function checkAllComments (url: string, token: string, videoUUID: string) {
-  const resThreads = await getVideoCommentThreads(url, videoUUID, 0, 25, '-createdAt', token)
+async function checkAllComments (server: PeerTubeServer, token: string, videoUUID: string) {
+  const { data } = await server.comments.listThreads({ videoId: videoUUID, start: 0, count: 25, sort: '-createdAt', token })
 
-  const allThreads: VideoComment[] = resThreads.body.data
-  const threads = allThreads.filter(t => t.isDeleted === false)
+  const threads = data.filter(t => t.isDeleted === false)
   expect(threads).to.have.lengthOf(2)
 
   for (const thread of threads) {
-    const res = await getVideoThreadComments(url, videoUUID, thread.id, token)
-
-    const tree: VideoCommentThreadTree = res.body
+    const tree = await server.comments.getThread({ videoId: videoUUID, threadId: thread.id, token })
     expect(tree.children).to.have.lengthOf(1)
   }
 }
 
 async function checkCommentNotification (
-  mainServer: ServerInfo,
-  comment: { server: ServerInfo, token: string, videoUUID: string, text: string },
+  mainServer: PeerTubeServer,
+  comment: { server: PeerTubeServer, token: string, videoUUID: string, text: string },
   check: 'presence' | 'absence'
 ) {
-  const resComment = await addVideoCommentThread(comment.server.url, comment.token, comment.videoUUID, comment.text)
-  const created = resComment.body.comment as VideoComment
-  const threadId = created.id
-  const createdAt = created.createdAt
+  const command = comment.server.comments
+
+  const { threadId, createdAt } = await command.createThread({ token: comment.token, videoId: comment.videoUUID, text: comment.text })
 
   await waitJobs([ mainServer, comment.server ])
 
-  const res = await getUserNotifications(mainServer.url, mainServer.accessToken, 0, 30)
-  const commentNotifications = (res.body.data as UserNotification[])
-                                  .filter(n => n.comment && n.comment.video.uuid === comment.videoUUID && n.createdAt >= createdAt)
+  const { data } = await mainServer.notifications.list({ start: 0, count: 30 })
+  const commentNotifications = data.filter(n => n.comment && n.comment.video.uuid === comment.videoUUID && n.createdAt >= createdAt)
 
   if (check === 'presence') expect(commentNotifications).to.have.lengthOf(1)
   else expect(commentNotifications).to.have.lengthOf(0)
 
-  await deleteVideoComment(comment.server.url, comment.token, comment.videoUUID, threadId)
+  await command.delete({ token: comment.token, videoId: comment.videoUUID, commentId: threadId })
 
   await waitJobs([ mainServer, comment.server ])
 }
 
 describe('Test blocklist', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let videoUUID1: string
   let videoUUID2: string
   let videoUUID3: string
@@ -110,62 +71,73 @@ describe('Test blocklist', function () {
   let userModeratorToken: string
   let userToken2: string
 
+  let command: BlocklistCommand
+  let commentsCommand: CommentsCommand[]
+
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
     await setAccessTokensToServers(servers)
 
+    command = servers[0].blocklist
+    commentsCommand = servers.map(s => s.comments)
+
     {
       const user = { username: 'user1', password: 'password' }
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
+      await servers[0].users.create({ username: user.username, password: user.password })
 
-      userToken1 = await userLogin(servers[0], user)
-      await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' })
+      userToken1 = await servers[0].login.getAccessToken(user)
+      await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video user 1' } })
     }
 
     {
       const user = { username: 'moderator', password: 'password' }
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
+      await servers[0].users.create({ username: user.username, password: user.password })
 
-      userModeratorToken = await userLogin(servers[0], user)
+      userModeratorToken = await servers[0].login.getAccessToken(user)
     }
 
     {
       const user = { username: 'user2', password: 'password' }
-      await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
+      await servers[1].users.create({ username: user.username, password: user.password })
 
-      userToken2 = await userLogin(servers[1], user)
-      await uploadVideo(servers[1].url, userToken2, { name: 'video user 2' })
+      userToken2 = await servers[1].login.getAccessToken(user)
+      await servers[1].videos.upload({ token: userToken2, attributes: { name: 'video user 2' } })
     }
 
     {
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video server 1' })
-      videoUUID1 = res.body.video.uuid
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video server 1' } })
+      videoUUID1 = uuid
     }
 
     {
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video server 2' })
-      videoUUID2 = res.body.video.uuid
+      const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video server 2' } })
+      videoUUID2 = uuid
     }
 
     {
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 2 server 1' })
-      videoUUID3 = res.body.video.uuid
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 2 server 1' } })
+      videoUUID3 = uuid
     }
 
     await doubleFollow(servers[0], servers[1])
     await doubleFollow(servers[0], servers[2])
 
     {
-      const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID1, 'comment root 1')
-      const resReply = await addVideoCommentReply(servers[0].url, userToken1, videoUUID1, resComment.body.comment.id, 'comment user 1')
-      await addVideoCommentReply(servers[0].url, servers[0].accessToken, videoUUID1, resReply.body.comment.id, 'comment root 1')
+      const created = await commentsCommand[0].createThread({ videoId: videoUUID1, text: 'comment root 1' })
+      const reply = await commentsCommand[0].addReply({
+        token: userToken1,
+        videoId: videoUUID1,
+        toCommentId: created.id,
+        text: 'comment user 1'
+      })
+      await commentsCommand[0].addReply({ videoId: videoUUID1, toCommentId: reply.id, text: 'comment root 1' })
     }
 
     {
-      const resComment = await addVideoCommentThread(servers[0].url, userToken1, videoUUID1, 'comment user 1')
-      await addVideoCommentReply(servers[0].url, servers[0].accessToken, videoUUID1, resComment.body.comment.id, 'comment root 1')
+      const created = await commentsCommand[0].createThread({ token: userToken1, videoId: videoUUID1, text: 'comment user 1' })
+      await commentsCommand[0].addReply({ videoId: videoUUID1, toCommentId: created.id, text: 'comment root 1' })
     }
 
     await waitJobs(servers)
@@ -175,55 +147,60 @@ describe('Test blocklist', function () {
 
     describe('When managing account blocklist', function () {
       it('Should list all videos', function () {
-        return checkAllVideos(servers[0].url, servers[0].accessToken)
+        return checkAllVideos(servers[0], servers[0].accessToken)
       })
 
       it('Should list the comments', function () {
-        return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
+        return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
       })
 
       it('Should block a remote account', async function () {
-        await addAccountToAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port)
+        await command.addToMyBlocklist({ account: 'user2@localhost:' + servers[1].port })
       })
 
       it('Should hide its videos', async function () {
-        const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
+        const { data } = await servers[0].videos.listWithToken()
 
-        const videos: Video[] = res.body.data
-        expect(videos).to.have.lengthOf(4)
+        expect(data).to.have.lengthOf(4)
 
-        const v = videos.find(v => v.name === 'video user 2')
+        const v = data.find(v => v.name === 'video user 2')
         expect(v).to.be.undefined
       })
 
       it('Should block a local account', async function () {
-        await addAccountToAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1')
+        await command.addToMyBlocklist({ account: 'user1' })
       })
 
       it('Should hide its videos', async function () {
-        const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
+        const { data } = await servers[0].videos.listWithToken()
 
-        const videos: Video[] = res.body.data
-        expect(videos).to.have.lengthOf(3)
+        expect(data).to.have.lengthOf(3)
 
-        const v = videos.find(v => v.name === 'video user 1')
+        const v = data.find(v => v.name === 'video user 1')
         expect(v).to.be.undefined
       })
 
       it('Should hide its comments', async function () {
-        const resThreads = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 25, '-createdAt', servers[0].accessToken)
-
-        const threads: VideoComment[] = resThreads.body.data
-        expect(threads).to.have.lengthOf(1)
-        expect(threads[0].totalReplies).to.equal(1)
-
-        const t = threads.find(t => t.text === 'comment user 1')
+        const { data } = await commentsCommand[0].listThreads({
+          token: servers[0].accessToken,
+          videoId: videoUUID1,
+          start: 0,
+          count: 25,
+          sort: '-createdAt'
+        })
+
+        expect(data).to.have.lengthOf(1)
+        expect(data[0].totalReplies).to.equal(1)
+
+        const t = data.find(t => t.text === 'comment user 1')
         expect(t).to.be.undefined
 
-        for (const thread of threads) {
-          const res = await getVideoThreadComments(servers[0].url, videoUUID1, thread.id, servers[0].accessToken)
-
-          const tree: VideoCommentThreadTree = res.body
+        for (const thread of data) {
+          const tree = await commentsCommand[0].getThread({
+            videoId: videoUUID1,
+            threadId: thread.id,
+            token: servers[0].accessToken
+          })
           expect(tree.children).to.have.lengthOf(0)
         }
       })
@@ -248,17 +225,15 @@ describe('Test blocklist', function () {
       })
 
       it('Should list all the videos with another user', async function () {
-        return checkAllVideos(servers[0].url, userToken1)
+        return checkAllVideos(servers[0], userToken1)
       })
 
       it('Should list blocked accounts', async function () {
         {
-          const res = await getAccountBlocklistByAccount(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt')
-          const blocks: AccountBlock[] = res.body.data
+          const body = await command.listMyAccountBlocklist({ start: 0, count: 1, sort: 'createdAt' })
+          expect(body.total).to.equal(2)
 
-          expect(res.body.total).to.equal(2)
-
-          const block = blocks[0]
+          const block = body.data[0]
           expect(block.byAccount.displayName).to.equal('root')
           expect(block.byAccount.name).to.equal('root')
           expect(block.blockedAccount.displayName).to.equal('user2')
@@ -267,12 +242,10 @@ describe('Test blocklist', function () {
         }
 
         {
-          const res = await getAccountBlocklistByAccount(servers[0].url, servers[0].accessToken, 1, 2, 'createdAt')
-          const blocks: AccountBlock[] = res.body.data
-
-          expect(res.body.total).to.equal(2)
+          const body = await command.listMyAccountBlocklist({ start: 1, count: 2, sort: 'createdAt' })
+          expect(body.total).to.equal(2)
 
-          const block = blocks[0]
+          const block = body.data[0]
           expect(block.byAccount.displayName).to.equal('root')
           expect(block.byAccount.name).to.equal('root')
           expect(block.blockedAccount.displayName).to.equal('user1')
@@ -285,32 +258,29 @@ describe('Test blocklist', function () {
         this.timeout(60000)
 
         {
-          await addVideoCommentThread(servers[1].url, userToken2, videoUUID3, 'comment user 2')
+          await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID3, text: 'comment user 2' })
           await waitJobs(servers)
 
-          await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID3, 'uploader')
+          await commentsCommand[0].createThread({ token: servers[0].accessToken, videoId: videoUUID3, text: 'uploader' })
           await waitJobs(servers)
 
-          const commentId = await findCommentId(servers[1].url, videoUUID3, 'uploader')
+          const commentId = await commentsCommand[1].findCommentId({ videoId: videoUUID3, text: 'uploader' })
           const message = 'reply by user 2'
-          const resReply = await addVideoCommentReply(servers[1].url, userToken2, videoUUID3, commentId, message)
-          await addVideoCommentReply(servers[1].url, servers[1].accessToken, videoUUID3, resReply.body.comment.id, 'another reply')
+          const reply = await commentsCommand[1].addReply({ token: userToken2, videoId: videoUUID3, toCommentId: commentId, text: message })
+          await commentsCommand[1].addReply({ videoId: videoUUID3, toCommentId: reply.id, text: 'another reply' })
 
           await waitJobs(servers)
         }
 
         // Server 2 has all the comments
         {
-          const resThreads = await getVideoCommentThreads(servers[1].url, videoUUID3, 0, 25, '-createdAt')
-          const threads: VideoComment[] = resThreads.body.data
-
-          expect(threads).to.have.lengthOf(2)
-          expect(threads[0].text).to.equal('uploader')
-          expect(threads[1].text).to.equal('comment user 2')
+          const { data } = await commentsCommand[1].listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' })
 
-          const resReplies = await getVideoThreadComments(servers[1].url, videoUUID3, threads[0].id)
+          expect(data).to.have.lengthOf(2)
+          expect(data[0].text).to.equal('uploader')
+          expect(data[1].text).to.equal('comment user 2')
 
-          const tree: VideoCommentThreadTree = resReplies.body
+          const tree = await commentsCommand[1].getThread({ videoId: videoUUID3, threadId: data[0].id })
           expect(tree.children).to.have.lengthOf(1)
           expect(tree.children[0].comment.text).to.equal('reply by user 2')
           expect(tree.children[0].children).to.have.lengthOf(1)
@@ -319,55 +289,45 @@ describe('Test blocklist', function () {
 
         // Server 1 and 3 should only have uploader comments
         for (const server of [ servers[0], servers[2] ]) {
-          const resThreads = await getVideoCommentThreads(server.url, videoUUID3, 0, 25, '-createdAt')
-          const threads: VideoComment[] = resThreads.body.data
+          const { data } = await server.comments.listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' })
 
-          expect(threads).to.have.lengthOf(1)
-          expect(threads[0].text).to.equal('uploader')
+          expect(data).to.have.lengthOf(1)
+          expect(data[0].text).to.equal('uploader')
 
-          const resReplies = await getVideoThreadComments(server.url, videoUUID3, threads[0].id)
+          const tree = await server.comments.getThread({ videoId: videoUUID3, threadId: data[0].id })
 
-          const tree: VideoCommentThreadTree = resReplies.body
-          if (server.serverNumber === 1) {
-            expect(tree.children).to.have.lengthOf(0)
-          } else {
-            expect(tree.children).to.have.lengthOf(1)
-          }
+          if (server.serverNumber === 1) expect(tree.children).to.have.lengthOf(0)
+          else expect(tree.children).to.have.lengthOf(1)
         }
       })
 
       it('Should unblock the remote account', async function () {
-        await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port)
+        await command.removeFromMyBlocklist({ account: 'user2@localhost:' + servers[1].port })
       })
 
       it('Should display its videos', async function () {
-        const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
-
-        const videos: Video[] = res.body.data
-        expect(videos).to.have.lengthOf(4)
+        const { data } = await servers[0].videos.listWithToken()
+        expect(data).to.have.lengthOf(4)
 
-        const v = videos.find(v => v.name === 'video user 2')
+        const v = data.find(v => v.name === 'video user 2')
         expect(v).not.to.be.undefined
       })
 
       it('Should display its comments on my video', async function () {
         for (const server of servers) {
-          const resThreads = await getVideoCommentThreads(server.url, videoUUID3, 0, 25, '-createdAt')
-          const threads: VideoComment[] = resThreads.body.data
+          const { data } = await server.comments.listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' })
 
           // Server 3 should not have 2 comment threads, because server 1 did not forward the server 2 comment
           if (server.serverNumber === 3) {
-            expect(threads).to.have.lengthOf(1)
+            expect(data).to.have.lengthOf(1)
             continue
           }
 
-          expect(threads).to.have.lengthOf(2)
-          expect(threads[0].text).to.equal('uploader')
-          expect(threads[1].text).to.equal('comment user 2')
+          expect(data).to.have.lengthOf(2)
+          expect(data[0].text).to.equal('uploader')
+          expect(data[1].text).to.equal('comment user 2')
 
-          const resReplies = await getVideoThreadComments(server.url, videoUUID3, threads[0].id)
-
-          const tree: VideoCommentThreadTree = resReplies.body
+          const tree = await server.comments.getThread({ videoId: videoUUID3, threadId: data[0].id })
           expect(tree.children).to.have.lengthOf(1)
           expect(tree.children[0].comment.text).to.equal('reply by user 2')
           expect(tree.children[0].children).to.have.lengthOf(1)
@@ -376,11 +336,11 @@ describe('Test blocklist', function () {
       })
 
       it('Should unblock the local account', async function () {
-        await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1')
+        await command.removeFromMyBlocklist({ account: 'user1' })
       })
 
       it('Should display its comments', function () {
-        return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
+        return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
       })
 
       it('Should have a notification from a non blocked account', async function () {
@@ -404,46 +364,45 @@ describe('Test blocklist', function () {
     })
 
     describe('When managing server blocklist', function () {
+
       it('Should list all videos', function () {
-        return checkAllVideos(servers[0].url, servers[0].accessToken)
+        return checkAllVideos(servers[0], servers[0].accessToken)
       })
 
       it('Should list the comments', function () {
-        return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
+        return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
       })
 
       it('Should block a remote server', async function () {
-        await addServerToAccountBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
+        await command.addToMyBlocklist({ server: 'localhost:' + servers[1].port })
       })
 
       it('Should hide its videos', async function () {
-        const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
+        const { data } = await servers[0].videos.listWithToken()
 
-        const videos: Video[] = res.body.data
-        expect(videos).to.have.lengthOf(3)
+        expect(data).to.have.lengthOf(3)
 
-        const v1 = videos.find(v => v.name === 'video user 2')
-        const v2 = videos.find(v => v.name === 'video server 2')
+        const v1 = data.find(v => v.name === 'video user 2')
+        const v2 = data.find(v => v.name === 'video server 2')
 
         expect(v1).to.be.undefined
         expect(v2).to.be.undefined
       })
 
       it('Should list all the videos with another user', async function () {
-        return checkAllVideos(servers[0].url, userToken1)
+        return checkAllVideos(servers[0], userToken1)
       })
 
       it('Should hide its comments', async function () {
         this.timeout(10000)
 
-        const resThreads = await addVideoCommentThread(servers[1].url, userToken2, videoUUID1, 'hidden comment 2')
-        const threadId = resThreads.body.comment.id
+        const { id } = await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID1, text: 'hidden comment 2' })
 
         await waitJobs(servers)
 
-        await checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
+        await checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
 
-        await deleteVideoComment(servers[1].url, userToken2, videoUUID1, threadId)
+        await commentsCommand[1].delete({ token: userToken2, videoId: videoUUID1, commentId: id })
       })
 
       it('Should not have notifications from blocked server', async function () {
@@ -466,27 +425,25 @@ describe('Test blocklist', function () {
       })
 
       it('Should list blocked servers', async function () {
-        const res = await getServerBlocklistByAccount(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt')
-        const blocks: ServerBlock[] = res.body.data
-
-        expect(res.body.total).to.equal(1)
+        const body = await command.listMyServerBlocklist({ start: 0, count: 1, sort: 'createdAt' })
+        expect(body.total).to.equal(1)
 
-        const block = blocks[0]
+        const block = body.data[0]
         expect(block.byAccount.displayName).to.equal('root')
         expect(block.byAccount.name).to.equal('root')
         expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
       })
 
       it('Should unblock the remote server', async function () {
-        await removeServerFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
+        await command.removeFromMyBlocklist({ server: 'localhost:' + servers[1].port })
       })
 
       it('Should display its videos', function () {
-        return checkAllVideos(servers[0].url, servers[0].accessToken)
+        return checkAllVideos(servers[0], servers[0].accessToken)
       })
 
       it('Should display its comments', function () {
-        return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
+        return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
       })
 
       it('Should have notification from unblocked server', async function () {
@@ -515,54 +472,50 @@ describe('Test blocklist', function () {
     describe('When managing account blocklist', function () {
       it('Should list all videos', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          await checkAllVideos(servers[0].url, token)
+          await checkAllVideos(servers[0], token)
         }
       })
 
       it('Should list the comments', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          await checkAllComments(servers[0].url, token, videoUUID1)
+          await checkAllComments(servers[0], token, videoUUID1)
         }
       })
 
       it('Should block a remote account', async function () {
-        await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port)
+        await command.addToServerBlocklist({ account: 'user2@localhost:' + servers[1].port })
       })
 
       it('Should hide its videos', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          const res = await getVideosListWithToken(servers[0].url, token)
+          const { data } = await servers[0].videos.listWithToken({ token })
 
-          const videos: Video[] = res.body.data
-          expect(videos).to.have.lengthOf(4)
+          expect(data).to.have.lengthOf(4)
 
-          const v = videos.find(v => v.name === 'video user 2')
+          const v = data.find(v => v.name === 'video user 2')
           expect(v).to.be.undefined
         }
       })
 
       it('Should block a local account', async function () {
-        await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'user1')
+        await command.addToServerBlocklist({ account: 'user1' })
       })
 
       it('Should hide its videos', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          const res = await getVideosListWithToken(servers[0].url, token)
+          const { data } = await servers[0].videos.listWithToken({ token })
 
-          const videos: Video[] = res.body.data
-          expect(videos).to.have.lengthOf(3)
+          expect(data).to.have.lengthOf(3)
 
-          const v = videos.find(v => v.name === 'video user 1')
+          const v = data.find(v => v.name === 'video user 1')
           expect(v).to.be.undefined
         }
       })
 
       it('Should hide its comments', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          const resThreads = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 20, '-createdAt', token)
-
-          let threads: VideoComment[] = resThreads.body.data
-          threads = threads.filter(t => t.isDeleted === false)
+          const { data } = await commentsCommand[0].listThreads({ videoId: videoUUID1, count: 20, sort: '-createdAt', token })
+          const threads = data.filter(t => t.isDeleted === false)
 
           expect(threads).to.have.lengthOf(1)
           expect(threads[0].totalReplies).to.equal(1)
@@ -571,9 +524,7 @@ describe('Test blocklist', function () {
           expect(t).to.be.undefined
 
           for (const thread of threads) {
-            const res = await getVideoThreadComments(servers[0].url, videoUUID1, thread.id, token)
-
-            const tree: VideoCommentThreadTree = res.body
+            const tree = await commentsCommand[0].getThread({ videoId: videoUUID1, threadId: thread.id, token })
             expect(tree.children).to.have.lengthOf(0)
           }
         }
@@ -600,12 +551,10 @@ describe('Test blocklist', function () {
 
       it('Should list blocked accounts', async function () {
         {
-          const res = await getAccountBlocklistByServer(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt')
-          const blocks: AccountBlock[] = res.body.data
-
-          expect(res.body.total).to.equal(2)
+          const body = await command.listServerAccountBlocklist({ start: 0, count: 1, sort: 'createdAt' })
+          expect(body.total).to.equal(2)
 
-          const block = blocks[0]
+          const block = body.data[0]
           expect(block.byAccount.displayName).to.equal('peertube')
           expect(block.byAccount.name).to.equal('peertube')
           expect(block.blockedAccount.displayName).to.equal('user2')
@@ -614,12 +563,10 @@ describe('Test blocklist', function () {
         }
 
         {
-          const res = await getAccountBlocklistByServer(servers[0].url, servers[0].accessToken, 1, 2, 'createdAt')
-          const blocks: AccountBlock[] = res.body.data
+          const body = await command.listServerAccountBlocklist({ start: 1, count: 2, sort: 'createdAt' })
+          expect(body.total).to.equal(2)
 
-          expect(res.body.total).to.equal(2)
-
-          const block = blocks[0]
+          const block = body.data[0]
           expect(block.byAccount.displayName).to.equal('peertube')
           expect(block.byAccount.name).to.equal('peertube')
           expect(block.blockedAccount.displayName).to.equal('user1')
@@ -629,28 +576,26 @@ describe('Test blocklist', function () {
       })
 
       it('Should unblock the remote account', async function () {
-        await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port)
+        await command.removeFromServerBlocklist({ account: 'user2@localhost:' + servers[1].port })
       })
 
       it('Should display its videos', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          const res = await getVideosListWithToken(servers[0].url, token)
-
-          const videos: Video[] = res.body.data
-          expect(videos).to.have.lengthOf(4)
+          const { data } = await servers[0].videos.listWithToken({ token })
+          expect(data).to.have.lengthOf(4)
 
-          const v = videos.find(v => v.name === 'video user 2')
+          const v = data.find(v => v.name === 'video user 2')
           expect(v).not.to.be.undefined
         }
       })
 
       it('Should unblock the local account', async function () {
-        await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, 'user1')
+        await command.removeFromServerBlocklist({ account: 'user1' })
       })
 
       it('Should display its comments', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          await checkAllComments(servers[0].url, token, videoUUID1)
+          await checkAllComments(servers[0], token, videoUUID1)
         }
       })
 
@@ -677,31 +622,33 @@ describe('Test blocklist', function () {
     describe('When managing server blocklist', function () {
       it('Should list all videos', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          await checkAllVideos(servers[0].url, token)
+          await checkAllVideos(servers[0], token)
         }
       })
 
       it('Should list the comments', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          await checkAllComments(servers[0].url, token, videoUUID1)
+          await checkAllComments(servers[0], token, videoUUID1)
         }
       })
 
       it('Should block a remote server', async function () {
-        await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
+        await command.addToServerBlocklist({ server: 'localhost:' + servers[1].port })
       })
 
       it('Should hide its videos', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          const res1 = await getVideosList(servers[0].url)
-          const res2 = await getVideosListWithToken(servers[0].url, token)
+          const requests = [
+            servers[0].videos.list(),
+            servers[0].videos.listWithToken({ token })
+          ]
 
-          for (const res of [ res1, res2 ]) {
-            const videos: Video[] = res.body.data
-            expect(videos).to.have.lengthOf(3)
+          for (const req of requests) {
+            const { data } = await req
+            expect(data).to.have.lengthOf(3)
 
-            const v1 = videos.find(v => v.name === 'video user 2')
-            const v2 = videos.find(v => v.name === 'video server 2')
+            const v1 = data.find(v => v.name === 'video user 2')
+            const v2 = data.find(v => v.name === 'video server 2')
 
             expect(v1).to.be.undefined
             expect(v2).to.be.undefined
@@ -712,14 +659,13 @@ describe('Test blocklist', function () {
       it('Should hide its comments', async function () {
         this.timeout(10000)
 
-        const resThreads = await addVideoCommentThread(servers[1].url, userToken2, videoUUID1, 'hidden comment 2')
-        const threadId = resThreads.body.comment.id
+        const { id } = await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID1, text: 'hidden comment 2' })
 
         await waitJobs(servers)
 
-        await checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
+        await checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
 
-        await deleteVideoComment(servers[1].url, userToken2, videoUUID1, threadId)
+        await commentsCommand[1].delete({ token: userToken2, videoId: videoUUID1, commentId: id })
       })
 
       it('Should not have notification from blocked instances by instance', async function () {
@@ -742,48 +688,44 @@ describe('Test blocklist', function () {
 
         {
           const now = new Date()
-          await unfollow(servers[1].url, servers[1].accessToken, servers[0])
+          await servers[1].follows.unfollow({ target: servers[0] })
           await waitJobs(servers)
-          await follow(servers[1].url, [ servers[0].host ], servers[1].accessToken)
+          await servers[1].follows.follow({ hosts: [ servers[0].host ] })
 
           await waitJobs(servers)
 
-          const res = await getUserNotifications(servers[0].url, servers[0].accessToken, 0, 30)
-          const commentNotifications = (res.body.data as UserNotification[])
-                                          .filter(n => {
-                                            return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER &&
-                                            n.createdAt >= now.toISOString()
-                                          })
+          const { data } = await servers[0].notifications.list({ start: 0, count: 30 })
+          const commentNotifications = data.filter(n => {
+            return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER && n.createdAt >= now.toISOString()
+          })
 
           expect(commentNotifications).to.have.lengthOf(0)
         }
       })
 
       it('Should list blocked servers', async function () {
-        const res = await getServerBlocklistByServer(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt')
-        const blocks: ServerBlock[] = res.body.data
-
-        expect(res.body.total).to.equal(1)
+        const body = await command.listServerServerBlocklist({ start: 0, count: 1, sort: 'createdAt' })
+        expect(body.total).to.equal(1)
 
-        const block = blocks[0]
+        const block = body.data[0]
         expect(block.byAccount.displayName).to.equal('peertube')
         expect(block.byAccount.name).to.equal('peertube')
         expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
       })
 
       it('Should unblock the remote server', async function () {
-        await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
+        await command.removeFromServerBlocklist({ server: 'localhost:' + servers[1].port })
       })
 
       it('Should list all videos', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          await checkAllVideos(servers[0].url, token)
+          await checkAllVideos(servers[0], token)
         }
       })
 
       it('Should list the comments', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
-          await checkAllComments(servers[0].url, token, videoUUID1)
+          await checkAllComments(servers[0], token, videoUUID1)
         }
       })
 
@@ -807,18 +749,16 @@ describe('Test blocklist', function () {
 
         {
           const now = new Date()
-          await unfollow(servers[1].url, servers[1].accessToken, servers[0])
+          await servers[1].follows.unfollow({ target: servers[0] })
           await waitJobs(servers)
-          await follow(servers[1].url, [ servers[0].host ], servers[1].accessToken)
+          await servers[1].follows.follow({ hosts: [ servers[0].host ] })
 
           await waitJobs(servers)
 
-          const res = await getUserNotifications(servers[0].url, servers[0].accessToken, 0, 30)
-          const commentNotifications = (res.body.data as UserNotification[])
-                                          .filter(n => {
-                                            return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER &&
-                                            n.createdAt >= now.toISOString()
-                                          })
+          const { data } = await servers[0].notifications.list({ start: 0, count: 30 })
+          const commentNotifications = data.filter(n => {
+            return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER && n.createdAt >= now.toISOString()
+          })
 
           expect(commentNotifications).to.have.lengthOf(1)
         }
index 52cac20d9e9ac0a9dd51a990289a69401ad8dbb3..d5838191ad47c04ba37d8b572e30ac0515f1ff95 100644 (file)
@@ -4,44 +4,30 @@ import 'mocha'
 import * as chai from 'chai'
 import { orderBy } from 'lodash'
 import {
-  addVideoToBlacklist,
+  BlacklistCommand,
   cleanupTests,
-  createUser,
-  flushAndRunMultipleServers,
-  getBlacklistedVideosList,
-  getMyUserInformation,
-  getMyVideos,
-  getVideosList,
+  createMultipleServers,
+  doubleFollow,
+  FIXTURE_URLS,
   killallServers,
-  removeVideoFromBlacklist,
-  reRunServer,
-  searchVideo,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updateVideo,
-  updateVideoBlacklist,
-  uploadVideo,
-  userLogin
-} from '../../../../shared/extra-utils/index'
-import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { getGoodVideoUrl, getMagnetURI, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
-import { User, UserRole } from '../../../../shared/models/users'
-import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
-import { VideoBlacklist, VideoBlacklistType } from '../../../../shared/models/videos'
+  waitJobs
+} from '@shared/extra-utils'
+import { UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test video blacklist', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let videoId: number
+  let command: BlacklistCommand
 
-  async function blacklistVideosOnServer (server: ServerInfo) {
-    const res = await getVideosList(server.url)
+  async function blacklistVideosOnServer (server: PeerTubeServer) {
+    const { data } = await server.videos.list()
 
-    const videos = res.body.data
-    for (const video of videos) {
-      await addVideoToBlacklist(server.url, server.accessToken, video.id, 'super reason')
+    for (const video of data) {
+      await server.blacklist.add({ videoId: video.id, reason: 'super reason' })
     }
   }
 
@@ -49,7 +35,7 @@ describe('Test video blacklist', function () {
     this.timeout(50000)
 
     // Run servers
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -58,12 +44,14 @@ describe('Test video blacklist', function () {
     await doubleFollow(servers[0], servers[1])
 
     // Upload 2 videos on server 2
-    await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 1st video', description: 'A video on server 2' })
-    await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 2nd video', description: 'A video on server 2' })
+    await servers[1].videos.upload({ attributes: { name: 'My 1st video', description: 'A video on server 2' } })
+    await servers[1].videos.upload({ attributes: { name: 'My 2nd video', description: 'A video on server 2' } })
 
     // Wait videos propagation, server 2 has transcoding enabled
     await waitJobs(servers)
 
+    command = servers[0].blacklist
+
     // Blacklist the two videos on server 1
     await blacklistVideosOnServer(servers[0])
   })
@@ -72,48 +60,47 @@ describe('Test video blacklist', function () {
 
     it('Should not have the video blacklisted in videos list/search on server 1', async function () {
       {
-        const res = await getVideosList(servers[0].url)
+        const { total, data } = await servers[0].videos.list()
 
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.be.an('array')
-        expect(res.body.data.length).to.equal(0)
+        expect(total).to.equal(0)
+        expect(data).to.be.an('array')
+        expect(data.length).to.equal(0)
       }
 
       {
-        const res = await searchVideo(servers[0].url, 'name')
+        const body = await servers[0].search.searchVideos({ search: 'video' })
 
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.be.an('array')
-        expect(res.body.data.length).to.equal(0)
+        expect(body.total).to.equal(0)
+        expect(body.data).to.be.an('array')
+        expect(body.data.length).to.equal(0)
       }
     })
 
     it('Should have the blacklisted video in videos list/search on server 2', async function () {
       {
-        const res = await getVideosList(servers[1].url)
+        const { total, data } = await servers[1].videos.list()
 
-        expect(res.body.total).to.equal(2)
-        expect(res.body.data).to.be.an('array')
-        expect(res.body.data.length).to.equal(2)
+        expect(total).to.equal(2)
+        expect(data).to.be.an('array')
+        expect(data.length).to.equal(2)
       }
 
       {
-        const res = await searchVideo(servers[1].url, 'video')
+        const body = await servers[1].search.searchVideos({ search: 'video' })
 
-        expect(res.body.total).to.equal(2)
-        expect(res.body.data).to.be.an('array')
-        expect(res.body.data.length).to.equal(2)
+        expect(body.total).to.equal(2)
+        expect(body.data).to.be.an('array')
+        expect(body.data.length).to.equal(2)
       }
     })
   })
 
   describe('When listing manually blacklisted videos', function () {
     it('Should display all the blacklisted videos', async function () {
-      const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken })
-
-      expect(res.body.total).to.equal(2)
+      const body = await command.list()
+      expect(body.total).to.equal(2)
 
-      const blacklistedVideos = res.body.data
+      const blacklistedVideos = body.data
       expect(blacklistedVideos).to.be.an('array')
       expect(blacklistedVideos.length).to.equal(2)
 
@@ -124,79 +111,66 @@ describe('Test video blacklist', function () {
     })
 
     it('Should display all the blacklisted videos when applying manual type filter', async function () {
-      const res = await getBlacklistedVideosList({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        type: VideoBlacklistType.MANUAL
-      })
+      const body = await command.list({ type: VideoBlacklistType.MANUAL })
+      expect(body.total).to.equal(2)
 
-      expect(res.body.total).to.equal(2)
-
-      const blacklistedVideos = res.body.data
+      const blacklistedVideos = body.data
       expect(blacklistedVideos).to.be.an('array')
       expect(blacklistedVideos.length).to.equal(2)
     })
 
     it('Should display nothing when applying automatic type filter', async function () {
-      const res = await getBlacklistedVideosList({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
-      })
-
-      expect(res.body.total).to.equal(0)
+      const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
+      expect(body.total).to.equal(0)
 
-      const blacklistedVideos = res.body.data
+      const blacklistedVideos = body.data
       expect(blacklistedVideos).to.be.an('array')
       expect(blacklistedVideos.length).to.equal(0)
     })
 
     it('Should get the correct sort when sorting by descending id', async function () {
-      const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: '-id' })
-      expect(res.body.total).to.equal(2)
+      const body = await command.list({ sort: '-id' })
+      expect(body.total).to.equal(2)
 
-      const blacklistedVideos = res.body.data
+      const blacklistedVideos = body.data
       expect(blacklistedVideos).to.be.an('array')
       expect(blacklistedVideos.length).to.equal(2)
 
-      const result = orderBy(res.body.data, [ 'id' ], [ 'desc' ])
-
+      const result = orderBy(body.data, [ 'id' ], [ 'desc' ])
       expect(blacklistedVideos).to.deep.equal(result)
     })
 
     it('Should get the correct sort when sorting by descending video name', async function () {
-      const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: '-name' })
-      expect(res.body.total).to.equal(2)
+      const body = await command.list({ sort: '-name' })
+      expect(body.total).to.equal(2)
 
-      const blacklistedVideos = res.body.data
+      const blacklistedVideos = body.data
       expect(blacklistedVideos).to.be.an('array')
       expect(blacklistedVideos.length).to.equal(2)
 
-      const result = orderBy(res.body.data, [ 'name' ], [ 'desc' ])
-
+      const result = orderBy(body.data, [ 'name' ], [ 'desc' ])
       expect(blacklistedVideos).to.deep.equal(result)
     })
 
     it('Should get the correct sort when sorting by ascending creation date', async function () {
-      const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: 'createdAt' })
-      expect(res.body.total).to.equal(2)
+      const body = await command.list({ sort: 'createdAt' })
+      expect(body.total).to.equal(2)
 
-      const blacklistedVideos = res.body.data
+      const blacklistedVideos = body.data
       expect(blacklistedVideos).to.be.an('array')
       expect(blacklistedVideos.length).to.equal(2)
 
-      const result = orderBy(res.body.data, [ 'createdAt' ])
-
+      const result = orderBy(body.data, [ 'createdAt' ])
       expect(blacklistedVideos).to.deep.equal(result)
     })
   })
 
   describe('When updating blacklisted videos', function () {
     it('Should change the reason', async function () {
-      await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated')
+      await command.update({ videoId, reason: 'my super reason updated' })
 
-      const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: '-name' })
-      const video = res.body.data.find(b => b.video.id === videoId)
+      const body = await command.list({ sort: '-name' })
+      const video = body.data.find(b => b.video.id === videoId)
 
       expect(video.reason).to.equal('my super reason updated')
     })
@@ -206,12 +180,12 @@ describe('Test video blacklist', function () {
     it('Should display blacklisted videos', async function () {
       await blacklistVideosOnServer(servers[1])
 
-      const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 5)
+      const { total, data } = await servers[1].videos.listMyVideos()
 
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.have.lengthOf(2)
+      expect(total).to.equal(2)
+      expect(data).to.have.lengthOf(2)
 
-      for (const video of res.body.data) {
+      for (const video of data) {
         expect(video.blacklisted).to.be.true
         expect(video.blacklistedReason).to.equal('super reason')
       }
@@ -223,39 +197,38 @@ describe('Test video blacklist', function () {
     let blacklist = []
 
     it('Should not have any video in videos list on server 1', async function () {
-      const res = await getVideosList(servers[0].url)
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data.length).to.equal(0)
+      const { total, data } = await servers[0].videos.list()
+      expect(total).to.equal(0)
+      expect(data).to.be.an('array')
+      expect(data.length).to.equal(0)
     })
 
     it('Should remove a video from the blacklist on server 1', async function () {
       // Get one video in the blacklist
-      const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: '-name' })
-      videoToRemove = res.body.data[0]
-      blacklist = res.body.data.slice(1)
+      const body = await command.list({ sort: '-name' })
+      videoToRemove = body.data[0]
+      blacklist = body.data.slice(1)
 
       // Remove it
-      await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoToRemove.video.id)
+      await command.remove({ videoId: videoToRemove.video.id })
     })
 
     it('Should have the ex-blacklisted video in videos list on server 1', async function () {
-      const res = await getVideosList(servers[0].url)
-      expect(res.body.total).to.equal(1)
+      const { total, data } = await servers[0].videos.list()
+      expect(total).to.equal(1)
 
-      const videos = res.body.data
-      expect(videos).to.be.an('array')
-      expect(videos.length).to.equal(1)
+      expect(data).to.be.an('array')
+      expect(data.length).to.equal(1)
 
-      expect(videos[0].name).to.equal(videoToRemove.video.name)
-      expect(videos[0].id).to.equal(videoToRemove.video.id)
+      expect(data[0].name).to.equal(videoToRemove.video.name)
+      expect(data[0].id).to.equal(videoToRemove.video.id)
     })
 
     it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () {
-      const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: '-name' })
-      expect(res.body.total).to.equal(1)
+      const body = await command.list({ sort: '-name' })
+      expect(body.total).to.equal(1)
 
-      const videos = res.body.data
+      const videos = body.data
       expect(videos).to.be.an('array')
       expect(videos.length).to.equal(1)
       expect(videos).to.deep.equal(blacklist)
@@ -270,12 +243,12 @@ describe('Test video blacklist', function () {
       this.timeout(10000)
 
       {
-        const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'Video 3' })
-        video3UUID = res.body.video.uuid
+        const { uuid } = await servers[0].videos.upload({ attributes: { name: 'Video 3' } })
+        video3UUID = uuid
       }
       {
-        const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'Video 4' })
-        video4UUID = res.body.video.uuid
+        const { uuid } = await servers[0].videos.upload({ attributes: { name: 'Video 4' } })
+        video4UUID = uuid
       }
 
       await waitJobs(servers)
@@ -284,51 +257,51 @@ describe('Test video blacklist', function () {
     it('Should blacklist video 3 and keep it federated', async function () {
       this.timeout(10000)
 
-      await addVideoToBlacklist(servers[0].url, servers[0].accessToken, video3UUID, 'super reason', false)
+      await command.add({ videoId: video3UUID, reason: 'super reason', unfederate: false })
 
       await waitJobs(servers)
 
       {
-        const res = await getVideosList(servers[0].url)
-        expect(res.body.data.find(v => v.uuid === video3UUID)).to.be.undefined
+        const { data } = await servers[0].videos.list()
+        expect(data.find(v => v.uuid === video3UUID)).to.be.undefined
       }
 
       {
-        const res = await getVideosList(servers[1].url)
-        expect(res.body.data.find(v => v.uuid === video3UUID)).to.not.be.undefined
+        const { data } = await servers[1].videos.list()
+        expect(data.find(v => v.uuid === video3UUID)).to.not.be.undefined
       }
     })
 
     it('Should unfederate the video', async function () {
       this.timeout(10000)
 
-      await addVideoToBlacklist(servers[0].url, servers[0].accessToken, video4UUID, 'super reason', true)
+      await command.add({ videoId: video4UUID, reason: 'super reason', unfederate: true })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-        expect(res.body.data.find(v => v.uuid === video4UUID)).to.be.undefined
+        const { data } = await server.videos.list()
+        expect(data.find(v => v.uuid === video4UUID)).to.be.undefined
       }
     })
 
     it('Should have the video unfederated even after an Update AP message', async function () {
       this.timeout(10000)
 
-      await updateVideo(servers[0].url, servers[0].accessToken, video4UUID, { description: 'super description' })
+      await servers[0].videos.update({ id: video4UUID, attributes: { description: 'super description' } })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-        expect(res.body.data.find(v => v.uuid === video4UUID)).to.be.undefined
+        const { data } = await server.videos.list()
+        expect(data.find(v => v.uuid === video4UUID)).to.be.undefined
       }
     })
 
     it('Should have the correct video blacklist unfederate attribute', async function () {
-      const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: 'createdAt' })
+      const body = await command.list({ sort: 'createdAt' })
 
-      const blacklistedVideos: VideoBlacklist[] = res.body.data
+      const blacklistedVideos = body.data
       const video3Blacklisted = blacklistedVideos.find(b => b.video.uuid === video3UUID)
       const video4Blacklisted = blacklistedVideos.find(b => b.video.uuid === video4UUID)
 
@@ -339,13 +312,13 @@ describe('Test video blacklist', function () {
     it('Should remove the video from blacklist and refederate the video', async function () {
       this.timeout(10000)
 
-      await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, video4UUID)
+      await command.remove({ videoId: video4UUID })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-        expect(res.body.data.find(v => v.uuid === video4UUID)).to.not.be.undefined
+        const { data } = await server.videos.list()
+        expect(data.find(v => v.uuid === video4UUID)).to.not.be.undefined
       }
     })
 
@@ -359,7 +332,7 @@ describe('Test video blacklist', function () {
     before(async function () {
       this.timeout(20000)
 
-      killallServers([ servers[0] ])
+      await killallServers([ servers[0] ])
 
       const config = {
         auto_blacklist: {
@@ -370,106 +343,79 @@ describe('Test video blacklist', function () {
           }
         }
       }
-      await reRunServer(servers[0], config)
+      await servers[0].run(config)
 
       {
         const user = { username: 'user_without_flag', password: 'password' }
-        await createUser({
-          url: servers[0].url,
-          accessToken: servers[0].accessToken,
+        await servers[0].users.create({
           username: user.username,
           adminFlags: UserAdminFlag.NONE,
           password: user.password,
           role: UserRole.USER
         })
 
-        userWithoutFlag = await userLogin(servers[0], user)
+        userWithoutFlag = await servers[0].login.getAccessToken(user)
 
-        const res = await getMyUserInformation(servers[0].url, userWithoutFlag)
-        const body: User = res.body
-        channelOfUserWithoutFlag = body.videoChannels[0].id
+        const { videoChannels } = await servers[0].users.getMyInfo({ token: userWithoutFlag })
+        channelOfUserWithoutFlag = videoChannels[0].id
       }
 
       {
         const user = { username: 'user_with_flag', password: 'password' }
-        await createUser({
-          url: servers[0].url,
-          accessToken: servers[0].accessToken,
+        await servers[0].users.create({
           username: user.username,
           adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST,
           password: user.password,
           role: UserRole.USER
         })
 
-        userWithFlag = await userLogin(servers[0], user)
+        userWithFlag = await servers[0].login.getAccessToken(user)
       }
 
       await waitJobs(servers)
     })
 
     it('Should auto blacklist a video on upload', async function () {
-      await uploadVideo(servers[0].url, userWithoutFlag, { name: 'blacklisted' })
-
-      const res = await getBlacklistedVideosList({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
-      })
+      await servers[0].videos.upload({ token: userWithoutFlag, attributes: { name: 'blacklisted' } })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data[0].video.name).to.equal('blacklisted')
+      const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
+      expect(body.total).to.equal(1)
+      expect(body.data[0].video.name).to.equal('blacklisted')
     })
 
     it('Should auto blacklist a video on URL import', async function () {
       this.timeout(15000)
 
       const attributes = {
-        targetUrl: getGoodVideoUrl(),
+        targetUrl: FIXTURE_URLS.goodVideo,
         name: 'URL import',
         channelId: channelOfUserWithoutFlag
       }
-      await importVideo(servers[0].url, userWithoutFlag, attributes)
+      await servers[0].imports.importVideo({ token: userWithoutFlag, attributes })
 
-      const res = await getBlacklistedVideosList({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        sort: 'createdAt',
-        type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
-      })
-
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data[1].video.name).to.equal('URL import')
+      const body = await command.list({ sort: 'createdAt', type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
+      expect(body.total).to.equal(2)
+      expect(body.data[1].video.name).to.equal('URL import')
     })
 
     it('Should auto blacklist a video on torrent import', async function () {
       const attributes = {
-        magnetUri: getMagnetURI(),
+        magnetUri: FIXTURE_URLS.magnet,
         name: 'Torrent import',
         channelId: channelOfUserWithoutFlag
       }
-      await importVideo(servers[0].url, userWithoutFlag, attributes)
-
-      const res = await getBlacklistedVideosList({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        sort: 'createdAt',
-        type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
-      })
+      await servers[0].imports.importVideo({ token: userWithoutFlag, attributes })
 
-      expect(res.body.total).to.equal(3)
-      expect(res.body.data[2].video.name).to.equal('Torrent import')
+      const body = await command.list({ sort: 'createdAt', type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
+      expect(body.total).to.equal(3)
+      expect(body.data[2].video.name).to.equal('Torrent import')
     })
 
     it('Should not auto blacklist a video on upload if the user has the bypass blacklist flag', async function () {
-      await uploadVideo(servers[0].url, userWithFlag, { name: 'not blacklisted' })
-
-      const res = await getBlacklistedVideosList({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
-      })
+      await servers[0].videos.upload({ token: userWithFlag, attributes: { name: 'not blacklisted' } })
 
-      expect(res.body.total).to.equal(3)
+      const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
+      expect(body.total).to.equal(3)
     })
   })
 
index cfe0bd2bbb0b94ef2144cdb3cd3d5216cf9d4083..c00d4e2575ae009b74415a54863b348d366870f8 100644 (file)
@@ -2,21 +2,21 @@
 
 import 'mocha'
 import { expect } from 'chai'
-import { MockJoinPeerTubeVersions } from '@shared/extra-utils/mock-servers/joinpeertube-versions'
-import { PluginType } from '@shared/models'
-import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils'
-import { ServerInfo } from '../../../../shared/extra-utils/index'
-import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
 import {
   CheckerBaseParams,
   checkNewPeerTubeVersion,
   checkNewPluginVersion,
-  prepareNotificationsTest
-} from '../../../../shared/extra-utils/users/user-notifications'
-import { UserNotification, UserNotificationType } from '../../../../shared/models/users'
+  cleanupTests,
+  MockJoinPeerTubeVersions,
+  MockSmtpServer,
+  PeerTubeServer,
+  prepareNotificationsTest,
+  wait
+} from '@shared/extra-utils'
+import { PluginType, UserNotification, UserNotificationType } from '@shared/models'
 
 describe('Test admin notifications', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userNotifications: UserNotification[] = []
   let adminNotifications: UserNotification[] = []
   let emails: object[] = []
@@ -58,17 +58,8 @@ describe('Test admin notifications', function () {
       token: server.accessToken
     }
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-plugin-hello-world'
-    })
-
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-theme-background-red'
-    })
+    await server.plugins.install({ npmName: 'peertube-plugin-hello-world' })
+    await server.plugins.install({ npmName: 'peertube-theme-background-red' })
   })
 
   describe('Latest PeerTube version notification', function () {
@@ -79,7 +70,7 @@ describe('Test admin notifications', function () {
       joinPeerTubeServer.setLatestVersion('1.4.2')
 
       await wait(3000)
-      await checkNewPeerTubeVersion(baseParams, '1.4.2', 'absence')
+      await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '1.4.2', checkType: 'absence' })
     })
 
     it('Should send a notification to admins on new plugin version', async function () {
@@ -88,7 +79,7 @@ describe('Test admin notifications', function () {
       joinPeerTubeServer.setLatestVersion('15.4.2')
 
       await wait(3000)
-      await checkNewPeerTubeVersion(baseParams, '15.4.2', 'presence')
+      await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.2', checkType: 'presence' })
     })
 
     it('Should not send the same notification to admins', async function () {
@@ -110,7 +101,7 @@ describe('Test admin notifications', function () {
       joinPeerTubeServer.setLatestVersion('15.4.3')
 
       await wait(3000)
-      await checkNewPeerTubeVersion(baseParams, '15.4.3', 'presence')
+      await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.3', checkType: 'presence' })
       expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
     })
   })
@@ -121,17 +112,17 @@ describe('Test admin notifications', function () {
       this.timeout(30000)
 
       await wait(6000)
-      await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'absence')
+      await checkNewPluginVersion({ ...baseParams, pluginType: PluginType.PLUGIN, pluginName: 'hello-world', checkType: 'absence' })
     })
 
     it('Should send a notification to admins on new plugin version', async function () {
       this.timeout(30000)
 
-      await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
-      await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+      await server.sql.setPluginVersion('hello-world', '0.0.1')
+      await server.sql.setPluginLatestVersion('hello-world', '0.0.1')
       await wait(6000)
 
-      await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'presence')
+      await checkNewPluginVersion({ ...baseParams, pluginType: PluginType.PLUGIN, pluginName: 'hello-world', checkType: 'presence' })
     })
 
     it('Should not send the same notification to admins', async function () {
@@ -149,8 +140,8 @@ describe('Test admin notifications', function () {
     it('Should send a new notification after a new plugin release', async function () {
       this.timeout(30000)
 
-      await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
-      await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+      await server.sql.setPluginVersion('hello-world', '0.0.1')
+      await server.sql.setPluginLatestVersion('hello-world', '0.0.1')
       await wait(6000)
 
       expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
index d2badf237403b542d282b3b52b30f8a6f9a68186..7cbb2139795be2b7072dff74cb4db60a411c4437 100644 (file)
@@ -3,30 +3,22 @@
 import 'mocha'
 import * as chai from 'chai'
 import {
-  addAccountToAccountBlocklist,
-  addVideoCommentReply,
-  addVideoCommentThread,
   checkCommentMention,
   CheckerBaseParams,
   checkNewCommentOnMyVideo,
   cleanupTests,
-  getVideoCommentThreads,
-  getVideoThreadComments,
   MockSmtpServer,
+  PeerTubeServer,
   prepareNotificationsTest,
-  removeAccountFromAccountBlocklist,
-  ServerInfo,
-  updateMyUser,
-  uploadVideo,
   waitJobs
 } from '@shared/extra-utils'
-import { UserNotification, VideoCommentThreadTree } from '@shared/models'
+import { UserNotification } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test comments notifications', function () {
-  let servers: ServerInfo[] = []
-  let userAccessToken: string
+  let servers: PeerTubeServer[] = []
+  let userToken: string
   let userNotifications: UserNotification[] = []
   let emails: object[] = []
 
@@ -40,7 +32,7 @@ describe('Test comments notifications', function () {
 
     const res = await prepareNotificationsTest(2)
     emails = res.emails
-    userAccessToken = res.userAccessToken
+    userToken = res.userAccessToken
     servers = res.servers
     userNotifications = res.userNotifications
   })
@@ -53,136 +45,125 @@ describe('Test comments notifications', function () {
         server: servers[0],
         emails,
         socketNotifications: userNotifications,
-        token: userAccessToken
+        token: userToken
       }
     })
 
     it('Should not send a new comment notification after a comment on another video', async function () {
       this.timeout(20000)
 
-      const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
 
-      const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment')
-      const commentId = resComment.body.comment.id
+      const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
+      const commentId = created.id
 
       await waitJobs(servers)
-      await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence')
+      await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' })
     })
 
     it('Should not send a new comment notification if I comment my own video', async function () {
       this.timeout(20000)
 
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
 
-      const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, uuid, 'comment')
-      const commentId = resComment.body.comment.id
+      const created = await servers[0].comments.createThread({ token: userToken, videoId: uuid, text: 'comment' })
+      const commentId = created.id
 
       await waitJobs(servers)
-      await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence')
+      await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' })
     })
 
     it('Should not send a new comment notification if the account is muted', async function () {
       this.timeout(20000)
 
-      await addAccountToAccountBlocklist(servers[0].url, userAccessToken, 'root')
+      await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' })
 
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
 
-      const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment')
-      const commentId = resComment.body.comment.id
+      const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
+      const commentId = created.id
 
       await waitJobs(servers)
-      await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence')
+      await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' })
 
-      await removeAccountFromAccountBlocklist(servers[0].url, userAccessToken, 'root')
+      await servers[0].blocklist.removeFromMyBlocklist({ token: userToken, account: 'root' })
     })
 
     it('Should send a new comment notification after a local comment on my video', async function () {
       this.timeout(20000)
 
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
 
-      const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment')
-      const commentId = resComment.body.comment.id
+      const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
+      const commentId = created.id
 
       await waitJobs(servers)
-      await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'presence')
+      await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'presence' })
     })
 
     it('Should send a new comment notification after a remote comment on my video', async function () {
       this.timeout(20000)
 
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
 
       await waitJobs(servers)
 
-      await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment')
+      await servers[1].comments.createThread({ videoId: uuid, text: 'comment' })
 
       await waitJobs(servers)
 
-      const resComment = await getVideoCommentThreads(servers[0].url, uuid, 0, 5)
-      expect(resComment.body.data).to.have.lengthOf(1)
-      const commentId = resComment.body.data[0].id
+      const { data } = await servers[0].comments.listThreads({ videoId: uuid })
+      expect(data).to.have.lengthOf(1)
 
-      await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'presence')
+      const commentId = data[0].id
+      await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'presence' })
     })
 
     it('Should send a new comment notification after a local reply on my video', async function () {
       this.timeout(20000)
 
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
 
-      const resThread = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment')
-      const threadId = resThread.body.comment.id
+      const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
 
-      const resComment = await addVideoCommentReply(servers[0].url, servers[0].accessToken, uuid, threadId, 'reply')
-      const commentId = resComment.body.comment.id
+      const { id: commentId } = await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'reply' })
 
       await waitJobs(servers)
-      await checkNewCommentOnMyVideo(baseParams, uuid, commentId, threadId, 'presence')
+      await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId, commentId, checkType: 'presence' })
     })
 
     it('Should send a new comment notification after a remote reply on my video', async function () {
       this.timeout(20000)
 
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
       await waitJobs(servers)
 
       {
-        const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment')
-        const threadId = resThread.body.comment.id
-        await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, threadId, 'reply')
+        const created = await servers[1].comments.createThread({ videoId: uuid, text: 'comment' })
+        const threadId = created.id
+        await servers[1].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'reply' })
       }
 
       await waitJobs(servers)
 
-      const resThread = await getVideoCommentThreads(servers[0].url, uuid, 0, 5)
-      expect(resThread.body.data).to.have.lengthOf(1)
-      const threadId = resThread.body.data[0].id
+      const { data } = await servers[0].comments.listThreads({ videoId: uuid })
+      expect(data).to.have.lengthOf(1)
 
-      const resComments = await getVideoThreadComments(servers[0].url, uuid, threadId)
-      const tree = resComments.body as VideoCommentThreadTree
+      const threadId = data[0].id
+      const tree = await servers[0].comments.getThread({ videoId: uuid, threadId })
 
       expect(tree.children).to.have.lengthOf(1)
       const commentId = tree.children[0].comment.id
 
-      await checkNewCommentOnMyVideo(baseParams, uuid, commentId, threadId, 'presence')
+      await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId, commentId, checkType: 'presence' })
     })
 
     it('Should convert markdown in comment to html', async function () {
       this.timeout(20000)
 
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'cool video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'cool video' } })
 
-      await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, commentText)
+      await servers[0].comments.createThread({ videoId: uuid, text: commentText })
 
       await waitJobs(servers)
 
@@ -193,147 +174,127 @@ describe('Test comments notifications', function () {
 
   describe('Mention notifications', function () {
     let baseParams: CheckerBaseParams
+    const byAccountDisplayName = 'super root name'
 
     before(async () => {
       baseParams = {
         server: servers[0],
         emails,
         socketNotifications: userNotifications,
-        token: userAccessToken
+        token: userToken
       }
 
-      await updateMyUser({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
-        displayName: 'super root name'
-      })
-
-      await updateMyUser({
-        url: servers[1].url,
-        accessToken: servers[1].accessToken,
-        displayName: 'super root 2 name'
-      })
+      await servers[0].users.updateMe({ displayName: 'super root name' })
+      await servers[1].users.updateMe({ displayName: 'super root 2 name' })
     })
 
     it('Should not send a new mention comment notification if I mention the video owner', async function () {
       this.timeout(10000)
 
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
 
-      const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello')
-      const commentId = resComment.body.comment.id
+      const { id: commentId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello' })
 
       await waitJobs(servers)
-      await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence')
+      await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' })
     })
 
     it('Should not send a new mention comment notification if I mention myself', async function () {
       this.timeout(10000)
 
-      const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
 
-      const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, uuid, '@user_1 hello')
-      const commentId = resComment.body.comment.id
+      const { id: commentId } = await servers[0].comments.createThread({ token: userToken, videoId: uuid, text: '@user_1 hello' })
 
       await waitJobs(servers)
-      await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence')
+      await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' })
     })
 
     it('Should not send a new mention notification if the account is muted', async function () {
       this.timeout(10000)
 
-      await addAccountToAccountBlocklist(servers[0].url, userAccessToken, 'root')
+      await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' })
 
-      const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
 
-      const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello')
-      const commentId = resComment.body.comment.id
+      const { id: commentId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello' })
 
       await waitJobs(servers)
-      await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence')
+      await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' })
 
-      await removeAccountFromAccountBlocklist(servers[0].url, userAccessToken, 'root')
+      await servers[0].blocklist.removeFromMyBlocklist({ token: userToken, account: 'root' })
     })
 
     it('Should not send a new mention notification if the remote account mention a local account', async function () {
       this.timeout(20000)
 
-      const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
 
       await waitJobs(servers)
-      const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, '@user_1 hello')
-      const threadId = resThread.body.comment.id
+      const { id: threadId } = await servers[1].comments.createThread({ videoId: uuid, text: '@user_1 hello' })
 
       await waitJobs(servers)
-      await checkCommentMention(baseParams, uuid, threadId, threadId, 'super root 2 name', 'absence')
+
+      const byAccountDisplayName = 'super root 2 name'
+      await checkCommentMention({ ...baseParams, shortUUID, threadId, commentId: threadId, byAccountDisplayName, checkType: 'absence' })
     })
 
     it('Should send a new mention notification after local comments', async function () {
       this.timeout(10000)
 
-      const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
 
-      const resThread = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello 1')
-      const threadId = resThread.body.comment.id
+      const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hellotext:  1' })
 
       await waitJobs(servers)
-      await checkCommentMention(baseParams, uuid, threadId, threadId, 'super root name', 'presence')
+      await checkCommentMention({ ...baseParams, shortUUID, threadId, commentId: threadId, byAccountDisplayName, checkType: 'presence' })
 
-      const resComment = await addVideoCommentReply(servers[0].url, servers[0].accessToken, uuid, threadId, 'hello 2 @user_1')
-      const commentId = resComment.body.comment.id
+      const { id: commentId } = await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'hello 2 @user_1' })
 
       await waitJobs(servers)
-      await checkCommentMention(baseParams, uuid, commentId, threadId, 'super root name', 'presence')
+      await checkCommentMention({ ...baseParams, shortUUID, commentId, threadId, byAccountDisplayName, checkType: 'presence' })
     })
 
     it('Should send a new mention notification after remote comments', async function () {
       this.timeout(20000)
 
-      const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
 
       await waitJobs(servers)
 
       const text1 = `hello @user_1@localhost:${servers[0].port} 1`
-      const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, text1)
-      const server2ThreadId = resThread.body.comment.id
+      const { id: server2ThreadId } = await servers[1].comments.createThread({ videoId: uuid, text: text1 })
 
       await waitJobs(servers)
 
-      const resThread2 = await getVideoCommentThreads(servers[0].url, uuid, 0, 5)
-      expect(resThread2.body.data).to.have.lengthOf(1)
-      const server1ThreadId = resThread2.body.data[0].id
-      await checkCommentMention(baseParams, uuid, server1ThreadId, server1ThreadId, 'super root 2 name', 'presence')
+      const { data } = await servers[0].comments.listThreads({ videoId: uuid })
+      expect(data).to.have.lengthOf(1)
+
+      const byAccountDisplayName = 'super root 2 name'
+      const threadId = data[0].id
+      await checkCommentMention({ ...baseParams, shortUUID, commentId: threadId, threadId, byAccountDisplayName, checkType: 'presence' })
 
       const text2 = `@user_1@localhost:${servers[0].port} hello 2 @root@localhost:${servers[0].port}`
-      await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, server2ThreadId, text2)
+      await servers[1].comments.addReply({ videoId: uuid, toCommentId: server2ThreadId, text: text2 })
 
       await waitJobs(servers)
 
-      const resComments = await getVideoThreadComments(servers[0].url, uuid, server1ThreadId)
-      const tree = resComments.body as VideoCommentThreadTree
+      const tree = await servers[0].comments.getThread({ videoId: uuid, threadId })
 
       expect(tree.children).to.have.lengthOf(1)
       const commentId = tree.children[0].comment.id
 
-      await checkCommentMention(baseParams, uuid, commentId, server1ThreadId, 'super root 2 name', 'presence')
+      await checkCommentMention({ ...baseParams, shortUUID, commentId, threadId, byAccountDisplayName, checkType: 'presence' })
     })
 
     it('Should convert markdown in comment to html', async function () {
       this.timeout(10000)
 
-      const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
-      const uuid = resVideo.body.video.uuid
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
 
-      const resThread = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello 1')
-      const threadId = resThread.body.comment.id
+      const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello 1' })
 
-      await addVideoCommentReply(servers[0].url, servers[0].accessToken, uuid, threadId, '@user_1 ' + commentText)
+      await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: '@user_1 ' + commentText })
 
       await waitJobs(servers)
 
index 3425480aeb314d1c0362c9b10b06edb963c18b87..eb3c29fe74532ce421e5a0ab11b880945a536783 100644 (file)
@@ -2,33 +2,6 @@
 
 import 'mocha'
 import { buildUUID } from '@server/helpers/uuid'
-import { AbuseState } from '@shared/models'
-import {
-  addAbuseMessage,
-  addVideoCommentThread,
-  addVideoToBlacklist,
-  cleanupTests,
-  createUser,
-  follow,
-  generateUserAccessToken,
-  getAccount,
-  getCustomConfig,
-  getVideoCommentThreads,
-  getVideoIdFromUUID,
-  immutableAssign,
-  MockInstancesIndex,
-  registerUser,
-  removeVideoFromBlacklist,
-  reportAbuse,
-  unfollow,
-  updateAbuse,
-  updateCustomConfig,
-  updateCustomSubConfig,
-  wait
-} from '../../../../shared/extra-utils'
-import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index'
-import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
 import {
   checkAbuseStateChange,
   checkAutoInstanceFollowing,
@@ -43,15 +16,18 @@ import {
   checkUserRegistered,
   checkVideoAutoBlacklistForModerators,
   checkVideoIsPublished,
-  prepareNotificationsTest
-} from '../../../../shared/extra-utils/users/user-notifications'
-import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions'
-import { CustomConfig } from '../../../../shared/models/server'
-import { UserNotification } from '../../../../shared/models/users'
-import { VideoPrivacy } from '../../../../shared/models/videos'
+  cleanupTests,
+  MockInstancesIndex,
+  MockSmtpServer,
+  PeerTubeServer,
+  prepareNotificationsTest,
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { AbuseState, CustomConfig, UserNotification, VideoPrivacy } from '@shared/models'
 
 describe('Test moderation notifications', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let userAccessToken: string
   let userNotifications: UserNotification[] = []
   let adminNotifications: UserNotification[] = []
@@ -86,93 +62,97 @@ describe('Test moderation notifications', function () {
       this.timeout(20000)
 
       const name = 'video for abuse ' + buildUUID()
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
-      const video = resVideo.body.video
+      const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } })
 
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: video.id, reason: 'super reason' })
+      await servers[0].abuses.report({ videoId: video.id, reason: 'super reason' })
 
       await waitJobs(servers)
-      await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence')
+      await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
     })
 
     it('Should send a notification to moderators on remote video abuse', async function () {
       this.timeout(20000)
 
       const name = 'video for abuse ' + buildUUID()
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
-      const video = resVideo.body.video
+      const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } })
 
       await waitJobs(servers)
 
-      const videoId = await getVideoIdFromUUID(servers[1].url, video.uuid)
-      await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId, reason: 'super reason' })
+      const videoId = await servers[1].videos.getId({ uuid: video.uuid })
+      await servers[1].abuses.report({ videoId, reason: 'super reason' })
 
       await waitJobs(servers)
-      await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence')
+      await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
     })
 
     it('Should send a notification to moderators on local comment abuse', async function () {
       this.timeout(20000)
 
       const name = 'video for abuse ' + buildUUID()
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
-      const video = resVideo.body.video
-      const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + buildUUID())
-      const comment = resComment.body.comment
+      const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } })
+      const comment = await servers[0].comments.createThread({
+        token: userAccessToken,
+        videoId: video.id,
+        text: 'comment abuse ' + buildUUID()
+      })
 
       await waitJobs(servers)
 
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason: 'super reason' })
+      await servers[0].abuses.report({ commentId: comment.id, reason: 'super reason' })
 
       await waitJobs(servers)
-      await checkNewCommentAbuseForModerators(baseParams, video.uuid, name, 'presence')
+      await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
     })
 
     it('Should send a notification to moderators on remote comment abuse', async function () {
       this.timeout(20000)
 
       const name = 'video for abuse ' + buildUUID()
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
-      const video = resVideo.body.video
-      await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + buildUUID())
+      const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } })
+
+      await servers[0].comments.createThread({
+        token: userAccessToken,
+        videoId: video.id,
+        text: 'comment abuse ' + buildUUID()
+      })
 
       await waitJobs(servers)
 
-      const resComments = await getVideoCommentThreads(servers[1].url, video.uuid, 0, 5)
-      const commentId = resComments.body.data[0].id
-      await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, commentId, reason: 'super reason' })
+      const { data } = await servers[1].comments.listThreads({ videoId: video.uuid })
+      const commentId = data[0].id
+      await servers[1].abuses.report({ commentId, reason: 'super reason' })
 
       await waitJobs(servers)
-      await checkNewCommentAbuseForModerators(baseParams, video.uuid, name, 'presence')
+      await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
     })
 
     it('Should send a notification to moderators on local account abuse', async function () {
       this.timeout(20000)
 
       const username = 'user' + new Date().getTime()
-      const resUser = await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username, password: 'donald' })
-      const accountId = resUser.body.user.account.id
+      const { account } = await servers[0].users.create({ username, password: 'donald' })
+      const accountId = account.id
 
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId, reason: 'super reason' })
+      await servers[0].abuses.report({ accountId, reason: 'super reason' })
 
       await waitJobs(servers)
-      await checkNewAccountAbuseForModerators(baseParams, username, 'presence')
+      await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' })
     })
 
     it('Should send a notification to moderators on remote account abuse', async function () {
       this.timeout(20000)
 
       const username = 'user' + new Date().getTime()
-      const tmpToken = await generateUserAccessToken(servers[0], username)
-      await uploadVideo(servers[0].url, tmpToken, { name: 'super video' })
+      const tmpToken = await servers[0].users.generateUserAndToken(username)
+      await servers[0].videos.upload({ token: tmpToken, attributes: { name: 'super video' } })
 
       await waitJobs(servers)
 
-      const resAccount = await getAccount(servers[1].url, username + '@' + servers[0].host)
-      await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, accountId: resAccount.body.id, reason: 'super reason' })
+      const account = await servers[1].accounts.get({ accountName: username + '@' + servers[0].host })
+      await servers[1].abuses.report({ accountId: account.id, reason: 'super reason' })
 
       await waitJobs(servers)
-      await checkNewAccountAbuseForModerators(baseParams, username, 'presence')
+      await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' })
     })
   })
 
@@ -189,29 +169,28 @@ describe('Test moderation notifications', function () {
       }
 
       const name = 'abuse ' + buildUUID()
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
-      const video = resVideo.body.video
+      const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } })
 
-      const res = await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: video.id, reason: 'super reason' })
-      abuseId = res.body.abuse.id
+      const body = await servers[0].abuses.report({ token: userAccessToken, videoId: video.id, reason: 'super reason' })
+      abuseId = body.abuse.id
     })
 
     it('Should send a notification to reporter if the abuse has been accepted', async function () {
       this.timeout(10000)
 
-      await updateAbuse(servers[0].url, servers[0].accessToken, abuseId, { state: AbuseState.ACCEPTED })
+      await servers[0].abuses.update({ abuseId, body: { state: AbuseState.ACCEPTED } })
       await waitJobs(servers)
 
-      await checkAbuseStateChange(baseParams, abuseId, AbuseState.ACCEPTED, 'presence')
+      await checkAbuseStateChange({ ...baseParams, abuseId, state: AbuseState.ACCEPTED, checkType: 'presence' })
     })
 
     it('Should send a notification to reporter if the abuse has been rejected', async function () {
       this.timeout(10000)
 
-      await updateAbuse(servers[0].url, servers[0].accessToken, abuseId, { state: AbuseState.REJECTED })
+      await servers[0].abuses.update({ abuseId, body: { state: AbuseState.REJECTED } })
       await waitJobs(servers)
 
-      await checkAbuseStateChange(baseParams, abuseId, AbuseState.REJECTED, 'presence')
+      await checkAbuseStateChange({ ...baseParams, abuseId, state: AbuseState.REJECTED, checkType: 'presence' })
     })
   })
 
@@ -237,17 +216,16 @@ describe('Test moderation notifications', function () {
       }
 
       const name = 'abuse ' + buildUUID()
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
-      const video = resVideo.body.video
+      const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } })
 
       {
-        const res = await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: video.id, reason: 'super reason' })
-        abuseId = res.body.abuse.id
+        const body = await servers[0].abuses.report({ token: userAccessToken, videoId: video.id, reason: 'super reason' })
+        abuseId = body.abuse.id
       }
 
       {
-        const res = await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: video.id, reason: 'super reason 2' })
-        abuseId2 = res.body.abuse.id
+        const body = await servers[0].abuses.report({ token: userAccessToken, videoId: video.id, reason: 'super reason 2' })
+        abuseId2 = body.abuse.id
       }
     })
 
@@ -255,40 +233,43 @@ describe('Test moderation notifications', function () {
       this.timeout(10000)
 
       const message = 'my super message to users'
-      await addAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, message)
+      await servers[0].abuses.addMessage({ abuseId, message })
       await waitJobs(servers)
 
-      await checkNewAbuseMessage(baseParamsUser, abuseId, message, 'user_1@example.com', 'presence')
+      await checkNewAbuseMessage({ ...baseParamsUser, abuseId, message, toEmail: 'user_1@example.com', checkType: 'presence' })
     })
 
     it('Should not send a notification to the admin if sent by the admin', async function () {
       this.timeout(10000)
 
       const message = 'my super message that should not be sent to the admin'
-      await addAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, message)
+      await servers[0].abuses.addMessage({ abuseId, message })
       await waitJobs(servers)
 
-      await checkNewAbuseMessage(baseParamsAdmin, abuseId, message, 'admin' + servers[0].internalServerNumber + '@example.com', 'absence')
+      const toEmail = 'admin' + servers[0].internalServerNumber + '@example.com'
+      await checkNewAbuseMessage({ ...baseParamsAdmin, abuseId, message, toEmail, checkType: 'absence' })
     })
 
     it('Should send a notification to moderators', async function () {
       this.timeout(10000)
 
       const message = 'my super message to moderators'
-      await addAbuseMessage(servers[0].url, userAccessToken, abuseId2, message)
+      await servers[0].abuses.addMessage({ token: userAccessToken, abuseId: abuseId2, message })
       await waitJobs(servers)
 
-      await checkNewAbuseMessage(baseParamsAdmin, abuseId2, message, 'admin' + servers[0].internalServerNumber + '@example.com', 'presence')
+      const toEmail = 'admin' + servers[0].internalServerNumber + '@example.com'
+      await checkNewAbuseMessage({ ...baseParamsAdmin, abuseId: abuseId2, message, toEmail, checkType: 'presence' })
     })
 
     it('Should not send a notification to reporter if sent by the reporter', async function () {
       this.timeout(10000)
 
       const message = 'my super message that should not be sent to reporter'
-      await addAbuseMessage(servers[0].url, userAccessToken, abuseId2, message)
+      await servers[0].abuses.addMessage({ token: userAccessToken, abuseId: abuseId2, message })
       await waitJobs(servers)
 
-      await checkNewAbuseMessage(baseParamsUser, abuseId2, message, 'user_1@example.com', 'absence')
+      const toEmail = 'user_1@example.com'
+      await checkNewAbuseMessage({ ...baseParamsUser, abuseId: abuseId2, message, toEmail, checkType: 'absence' })
     })
   })
 
@@ -308,30 +289,28 @@ describe('Test moderation notifications', function () {
       this.timeout(10000)
 
       const name = 'video for abuse ' + buildUUID()
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } })
 
-      await addVideoToBlacklist(servers[0].url, servers[0].accessToken, uuid)
+      await servers[0].blacklist.add({ videoId: uuid })
 
       await waitJobs(servers)
-      await checkNewBlacklistOnMyVideo(baseParams, uuid, name, 'blacklist')
+      await checkNewBlacklistOnMyVideo({ ...baseParams, shortUUID, videoName: name, blacklistType: 'blacklist' })
     })
 
     it('Should send a notification to video owner on unblacklist', async function () {
       this.timeout(10000)
 
       const name = 'video for abuse ' + buildUUID()
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
-      const uuid = resVideo.body.video.uuid
+      const { uuid, shortUUID } = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } })
 
-      await addVideoToBlacklist(servers[0].url, servers[0].accessToken, uuid)
+      await servers[0].blacklist.add({ videoId: uuid })
 
       await waitJobs(servers)
-      await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, uuid)
+      await servers[0].blacklist.remove({ videoId: uuid })
       await waitJobs(servers)
 
       await wait(500)
-      await checkNewBlacklistOnMyVideo(baseParams, uuid, name, 'unblacklist')
+      await checkNewBlacklistOnMyVideo({ ...baseParams, shortUUID, videoName: name, blacklistType: 'unblacklist' })
     })
   })
 
@@ -350,14 +329,14 @@ describe('Test moderation notifications', function () {
     it('Should send a notification only to moderators when a user registers on the instance', async function () {
       this.timeout(10000)
 
-      await registerUser(servers[0].url, 'user_45', 'password')
+      await servers[0].users.register({ username: 'user_45' })
 
       await waitJobs(servers)
 
-      await checkUserRegistered(baseParams, 'user_45', 'presence')
+      await checkUserRegistered({ ...baseParams, username: 'user_45', checkType: 'presence' })
 
       const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
-      await checkUserRegistered(immutableAssign(baseParams, userOverride), 'user_45', 'absence')
+      await checkUserRegistered({ ...baseParams, ...userOverride, username: 'user_45', checkType: 'absence' })
     })
   })
 
@@ -392,20 +371,20 @@ describe('Test moderation notifications', function () {
     it('Should send a notification only to admin when there is a new instance follower', async function () {
       this.timeout(20000)
 
-      await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
+      await servers[2].follows.follow({ hosts: [ servers[0].url ] })
 
       await waitJobs(servers)
 
-      await checkNewInstanceFollower(baseParams, 'localhost:' + servers[2].port, 'presence')
+      await checkNewInstanceFollower({ ...baseParams, followerHost: 'localhost:' + servers[2].port, checkType: 'presence' })
 
       const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
-      await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:' + servers[2].port, 'absence')
+      await checkNewInstanceFollower({ ...baseParams, ...userOverride, followerHost: 'localhost:' + servers[2].port, checkType: 'absence' })
     })
 
     it('Should send a notification on auto follow back', async function () {
       this.timeout(40000)
 
-      await unfollow(servers[2].url, servers[2].accessToken, servers[0])
+      await servers[2].follows.unfollow({ target: servers[0] })
       await waitJobs(servers)
 
       const config = {
@@ -415,41 +394,41 @@ describe('Test moderation notifications', function () {
           }
         }
       }
-      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+      await servers[0].config.updateCustomSubConfig({ newConfig: config })
 
-      await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
+      await servers[2].follows.follow({ hosts: [ servers[0].url ] })
 
       await waitJobs(servers)
 
       const followerHost = servers[0].host
       const followingHost = servers[2].host
-      await checkAutoInstanceFollowing(baseParams, followerHost, followingHost, 'presence')
+      await checkAutoInstanceFollowing({ ...baseParams, followerHost, followingHost, checkType: 'presence' })
 
       const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
-      await checkAutoInstanceFollowing(immutableAssign(baseParams, userOverride), followerHost, followingHost, 'absence')
+      await checkAutoInstanceFollowing({ ...baseParams, ...userOverride, followerHost, followingHost, checkType: 'absence' })
 
       config.followings.instance.autoFollowBack.enabled = false
-      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
-      await unfollow(servers[0].url, servers[0].accessToken, servers[2])
-      await unfollow(servers[2].url, servers[2].accessToken, servers[0])
+      await servers[0].config.updateCustomSubConfig({ newConfig: config })
+      await servers[0].follows.unfollow({ target: servers[2] })
+      await servers[2].follows.unfollow({ target: servers[0] })
     })
 
     it('Should send a notification on auto instances index follow', async function () {
       this.timeout(30000)
-      await unfollow(servers[0].url, servers[0].accessToken, servers[1])
+      await servers[0].follows.unfollow({ target: servers[1] })
 
-      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+      await servers[0].config.updateCustomSubConfig({ newConfig: config })
 
       await wait(5000)
       await waitJobs(servers)
 
       const followerHost = servers[0].host
       const followingHost = servers[1].host
-      await checkAutoInstanceFollowing(baseParams, followerHost, followingHost, 'presence')
+      await checkAutoInstanceFollowing({ ...baseParams, followerHost, followingHost, checkType: 'presence' })
 
       config.followings.instance.autoFollowIndex.enabled = false
-      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
-      await unfollow(servers[0].url, servers[0].accessToken, servers[1])
+      await servers[0].config.updateCustomSubConfig({ newConfig: config })
+      await servers[0].follows.unfollow({ target: servers[1] })
     })
   })
 
@@ -457,7 +436,8 @@ describe('Test moderation notifications', function () {
     let userBaseParams: CheckerBaseParams
     let adminBaseParamsServer1: CheckerBaseParams
     let adminBaseParamsServer2: CheckerBaseParams
-    let videoUUID: string
+    let uuid: string
+    let shortUUID: string
     let videoName: string
     let currentCustomConfig: CustomConfig
 
@@ -484,9 +464,11 @@ describe('Test moderation notifications', function () {
         token: userAccessToken
       }
 
-      const resCustomConfig = await getCustomConfig(servers[0].url, servers[0].accessToken)
-      currentCustomConfig = resCustomConfig.body
-      const autoBlacklistTestsCustomConfig = immutableAssign(currentCustomConfig, {
+      currentCustomConfig = await servers[0].config.getCustomConfig()
+
+      const autoBlacklistTestsCustomConfig = {
+        ...currentCustomConfig,
+
         autoBlacklist: {
           videos: {
             ofUsers: {
@@ -494,43 +476,44 @@ describe('Test moderation notifications', function () {
             }
           }
         }
-      })
+      }
+
       // enable transcoding otherwise own publish notification after transcoding not expected
       autoBlacklistTestsCustomConfig.transcoding.enabled = true
-      await updateCustomConfig(servers[0].url, servers[0].accessToken, autoBlacklistTestsCustomConfig)
-
-      await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:' + servers[0].port)
-      await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:' + servers[0].port)
+      await servers[0].config.updateCustomConfig({ newCustomConfig: autoBlacklistTestsCustomConfig })
 
+      await servers[0].subscriptions.add({ targetUri: 'user_1_channel@localhost:' + servers[0].port })
+      await servers[1].subscriptions.add({ targetUri: 'user_1_channel@localhost:' + servers[0].port })
     })
 
     it('Should send notification to moderators on new video with auto-blacklist', async function () {
       this.timeout(40000)
 
       videoName = 'video with auto-blacklist ' + buildUUID()
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName })
-      videoUUID = resVideo.body.video.uuid
+      const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name: videoName } })
+      shortUUID = video.shortUUID
+      uuid = video.uuid
 
       await waitJobs(servers)
-      await checkVideoAutoBlacklistForModerators(adminBaseParamsServer1, videoUUID, videoName, 'presence')
+      await checkVideoAutoBlacklistForModerators({ ...adminBaseParamsServer1, shortUUID, videoName, checkType: 'presence' })
     })
 
     it('Should not send video publish notification if auto-blacklisted', async function () {
-      await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'absence')
+      await checkVideoIsPublished({ ...userBaseParams, videoName, shortUUID, checkType: 'absence' })
     })
 
     it('Should not send a local user subscription notification if auto-blacklisted', async function () {
-      await checkNewVideoFromSubscription(adminBaseParamsServer1, videoName, videoUUID, 'absence')
+      await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'absence' })
     })
 
     it('Should not send a remote user subscription notification if auto-blacklisted', async function () {
-      await checkNewVideoFromSubscription(adminBaseParamsServer2, videoName, videoUUID, 'absence')
+      await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'absence' })
     })
 
     it('Should send video published and unblacklist after video unblacklisted', async function () {
       this.timeout(40000)
 
-      await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoUUID)
+      await servers[0].blacklist.remove({ videoId: uuid })
 
       await waitJobs(servers)
 
@@ -541,11 +524,11 @@ describe('Test moderation notifications', function () {
     })
 
     it('Should send a local user subscription notification after removed from blacklist', async function () {
-      await checkNewVideoFromSubscription(adminBaseParamsServer1, videoName, videoUUID, 'presence')
+      await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'presence' })
     })
 
     it('Should send a remote user subscription notification after removed from blacklist', async function () {
-      await checkNewVideoFromSubscription(adminBaseParamsServer2, videoName, videoUUID, 'presence')
+      await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'presence' })
     })
 
     it('Should send unblacklist but not published/subscription notes after unblacklisted if scheduled update pending', async function () {
@@ -555,29 +538,28 @@ describe('Test moderation notifications', function () {
 
       const name = 'video with auto-blacklist and future schedule ' + buildUUID()
 
-      const data = {
+      const attributes = {
         name,
         privacy: VideoPrivacy.PRIVATE,
         scheduleUpdate: {
           updateAt: updateAt.toISOString(),
-          privacy: VideoPrivacy.PUBLIC
+          privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC
         }
       }
 
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, data)
-      const uuid = resVideo.body.video.uuid
+      const { shortUUID, uuid } = await servers[0].videos.upload({ token: userAccessToken, attributes })
 
-      await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, uuid)
+      await servers[0].blacklist.remove({ videoId: uuid })
 
       await waitJobs(servers)
-      await checkNewBlacklistOnMyVideo(userBaseParams, uuid, name, 'unblacklist')
+      await checkNewBlacklistOnMyVideo({ ...userBaseParams, shortUUID, videoName: name, blacklistType: 'unblacklist' })
 
       // FIXME: Can't test absence as two notifications sent to same user and util only checks last one
       // One notification might be better anyways
       // await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
 
-      await checkNewVideoFromSubscription(adminBaseParamsServer1, name, uuid, 'absence')
-      await checkNewVideoFromSubscription(adminBaseParamsServer2, name, uuid, 'absence')
+      await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName: name, shortUUID, checkType: 'absence' })
+      await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName: name, shortUUID, checkType: 'absence' })
     })
 
     it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () {
@@ -588,22 +570,21 @@ describe('Test moderation notifications', function () {
 
       const name = 'video with schedule done and still auto-blacklisted ' + buildUUID()
 
-      const data = {
+      const attributes = {
         name,
         privacy: VideoPrivacy.PRIVATE,
         scheduleUpdate: {
           updateAt: updateAt.toISOString(),
-          privacy: VideoPrivacy.PUBLIC
+          privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC
         }
       }
 
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, data)
-      const uuid = resVideo.body.video.uuid
+      const { shortUUID } = await servers[0].videos.upload({ token: userAccessToken, attributes })
 
       await wait(6000)
-      await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
-      await checkNewVideoFromSubscription(adminBaseParamsServer1, name, uuid, 'absence')
-      await checkNewVideoFromSubscription(adminBaseParamsServer2, name, uuid, 'absence')
+      await checkVideoIsPublished({ ...userBaseParams, videoName: name, shortUUID, checkType: 'absence' })
+      await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName: name, shortUUID, checkType: 'absence' })
+      await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName: name, shortUUID, checkType: 'absence' })
     })
 
     it('Should not send a notification to moderators on new video without auto-blacklist', async function () {
@@ -612,18 +593,17 @@ describe('Test moderation notifications', function () {
       const name = 'video without auto-blacklist ' + buildUUID()
 
       // admin with blacklist right will not be auto-blacklisted
-      const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name })
-      const uuid = resVideo.body.video.uuid
+      const { shortUUID } = await servers[0].videos.upload({ attributes: { name } })
 
       await waitJobs(servers)
-      await checkVideoAutoBlacklistForModerators(adminBaseParamsServer1, uuid, name, 'absence')
+      await checkVideoAutoBlacklistForModerators({ ...adminBaseParamsServer1, shortUUID, videoName: name, checkType: 'absence' })
     })
 
     after(async () => {
-      await updateCustomConfig(servers[0].url, servers[0].accessToken, currentCustomConfig)
+      await servers[0].config.updateCustomConfig({ newCustomConfig: currentCustomConfig })
 
-      await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:' + servers[0].port)
-      await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:' + servers[0].port)
+      await servers[0].subscriptions.remove({ uri: 'user_1_channel@localhost:' + servers[0].port })
+      await servers[1].subscriptions.remove({ uri: 'user_1_channel@localhost:' + servers[0].port })
     })
   })
 
index b819954494a36461060ac85c280e37b3536a098a..a529a9bf7b78f73ce36919d1f03a344c94d305e4 100644 (file)
@@ -2,28 +2,24 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { addUserSubscription } from '@shared/extra-utils/users/user-subscriptions'
-import { cleanupTests, getMyUserInformation, immutableAssign, uploadRandomVideo, waitJobs } from '../../../../shared/extra-utils'
-import { ServerInfo } from '../../../../shared/extra-utils/index'
-import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
 import {
   CheckerBaseParams,
   checkNewVideoFromSubscription,
+  cleanupTests,
   getAllNotificationsSettings,
-  getUserNotifications,
-  markAsReadAllNotifications,
-  markAsReadNotifications,
+  MockSmtpServer,
+  PeerTubeServer,
   prepareNotificationsTest,
-  updateMyNotificationSettings
-} from '../../../../shared/extra-utils/users/user-notifications'
-import { User, UserNotification, UserNotificationSettingValue } from '../../../../shared/models/users'
+  waitJobs
+} from '@shared/extra-utils'
+import { UserNotification, UserNotificationSettingValue } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test notifications API', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userNotifications: UserNotification[] = []
-  let userAccessToken: string
+  let userToken: string
   let emails: object[] = []
 
   before(async function () {
@@ -31,14 +27,14 @@ describe('Test notifications API', function () {
 
     const res = await prepareNotificationsTest(1)
     emails = res.emails
-    userAccessToken = res.userAccessToken
+    userToken = res.userAccessToken
     userNotifications = res.userNotifications
     server = res.servers[0]
 
-    await addUserSubscription(server.url, userAccessToken, 'root_channel@localhost:' + server.port)
+    await server.subscriptions.add({ token: userToken, targetUri: 'root_channel@localhost:' + server.port })
 
     for (let i = 0; i < 10; i++) {
-      await uploadRandomVideo(server, false)
+      await server.videos.randomUpload({ wait: false })
     }
 
     await waitJobs([ server ])
@@ -47,49 +43,46 @@ describe('Test notifications API', function () {
   describe('Mark as read', function () {
 
     it('Should mark as read some notifications', async function () {
-      const res = await getUserNotifications(server.url, userAccessToken, 2, 3)
-      const ids = res.body.data.map(n => n.id)
+      const { data } = await server.notifications.list({ token: userToken, start: 2, count: 3 })
+      const ids = data.map(n => n.id)
 
-      await markAsReadNotifications(server.url, userAccessToken, ids)
+      await server.notifications.markAsRead({ token: userToken, ids })
     })
 
     it('Should have the notifications marked as read', async function () {
-      const res = await getUserNotifications(server.url, userAccessToken, 0, 10)
-
-      const notifications = res.body.data as UserNotification[]
-      expect(notifications[0].read).to.be.false
-      expect(notifications[1].read).to.be.false
-      expect(notifications[2].read).to.be.true
-      expect(notifications[3].read).to.be.true
-      expect(notifications[4].read).to.be.true
-      expect(notifications[5].read).to.be.false
+      const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10 })
+
+      expect(data[0].read).to.be.false
+      expect(data[1].read).to.be.false
+      expect(data[2].read).to.be.true
+      expect(data[3].read).to.be.true
+      expect(data[4].read).to.be.true
+      expect(data[5].read).to.be.false
     })
 
     it('Should only list read notifications', async function () {
-      const res = await getUserNotifications(server.url, userAccessToken, 0, 10, false)
+      const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: false })
 
-      const notifications = res.body.data as UserNotification[]
-      for (const notification of notifications) {
+      for (const notification of data) {
         expect(notification.read).to.be.true
       }
     })
 
     it('Should only list unread notifications', async function () {
-      const res = await getUserNotifications(server.url, userAccessToken, 0, 10, true)
+      const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: true })
 
-      const notifications = res.body.data as UserNotification[]
-      for (const notification of notifications) {
+      for (const notification of data) {
         expect(notification.read).to.be.false
       }
     })
 
     it('Should mark as read all notifications', async function () {
-      await markAsReadAllNotifications(server.url, userAccessToken)
+      await server.notifications.markAsReadAll({ token: userToken })
 
-      const res = await getUserNotifications(server.url, userAccessToken, 0, 10, true)
+      const body = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: true })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
     })
   })
 
@@ -101,99 +94,102 @@ describe('Test notifications API', function () {
         server: server,
         emails,
         socketNotifications: userNotifications,
-        token: userAccessToken
+        token: userToken
       }
     })
 
     it('Should not have notifications', async function () {
       this.timeout(20000)
 
-      await updateMyNotificationSettings(server.url, userAccessToken, immutableAssign(getAllNotificationsSettings(), {
-        newVideoFromSubscription: UserNotificationSettingValue.NONE
-      }))
+      await server.notifications.updateMySettings({
+        token: userToken,
+        settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.NONE }
+      })
 
       {
-        const res = await getMyUserInformation(server.url, userAccessToken)
-        const info = res.body as User
+        const info = await server.users.getMyInfo({ token: userToken })
         expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.NONE)
       }
 
-      const { name, uuid } = await uploadRandomVideo(server)
+      const { name, shortUUID } = await server.videos.randomUpload()
 
       const check = { web: true, mail: true }
-      await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence')
+      await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' })
     })
 
     it('Should only have web notifications', async function () {
       this.timeout(20000)
 
-      await updateMyNotificationSettings(server.url, userAccessToken, immutableAssign(getAllNotificationsSettings(), {
-        newVideoFromSubscription: UserNotificationSettingValue.WEB
-      }))
+      await server.notifications.updateMySettings({
+        token: userToken,
+        settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.WEB }
+      })
 
       {
-        const res = await getMyUserInformation(server.url, userAccessToken)
-        const info = res.body as User
+        const info = await server.users.getMyInfo({ token: userToken })
         expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB)
       }
 
-      const { name, uuid } = await uploadRandomVideo(server)
+      const { name, shortUUID } = await server.videos.randomUpload()
 
       {
         const check = { mail: true, web: false }
-        await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence')
+        await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' })
       }
 
       {
         const check = { mail: false, web: true }
-        await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'presence')
+        await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'presence' })
       }
     })
 
     it('Should only have mail notifications', async function () {
       this.timeout(20000)
 
-      await updateMyNotificationSettings(server.url, userAccessToken, immutableAssign(getAllNotificationsSettings(), {
-        newVideoFromSubscription: UserNotificationSettingValue.EMAIL
-      }))
+      await server.notifications.updateMySettings({
+        token: userToken,
+        settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.EMAIL }
+      })
 
       {
-        const res = await getMyUserInformation(server.url, userAccessToken)
-        const info = res.body as User
+        const info = await server.users.getMyInfo({ token: userToken })
         expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.EMAIL)
       }
 
-      const { name, uuid } = await uploadRandomVideo(server)
+      const { name, shortUUID } = await server.videos.randomUpload()
 
       {
         const check = { mail: false, web: true }
-        await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'absence')
+        await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' })
       }
 
       {
         const check = { mail: true, web: false }
-        await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), name, uuid, 'presence')
+        await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'presence' })
       }
     })
 
     it('Should have email and web notifications', async function () {
       this.timeout(20000)
 
-      await updateMyNotificationSettings(server.url, userAccessToken, immutableAssign(getAllNotificationsSettings(), {
-        newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
-      }))
+      await server.notifications.updateMySettings({
+        token: userToken,
+        settings: {
+          ...getAllNotificationsSettings(),
+          newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
+        }
+      })
 
       {
-        const res = await getMyUserInformation(server.url, userAccessToken)
-        const info = res.body as User
+        const info = await server.users.getMyInfo({ token: userToken })
         expect(info.notificationSettings.newVideoFromSubscription).to.equal(
           UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
         )
       }
 
-      const { name, uuid } = await uploadRandomVideo(server)
+      const { name, shortUUID } = await server.videos.randomUpload()
 
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
     })
   })
 
index e981c17189a672cd861db68652138e9e962e0343..e53ab2aa527a02117d5117442e23178c5423c2ed 100644 (file)
@@ -3,35 +3,27 @@
 import 'mocha'
 import * as chai from 'chai'
 import { buildUUID } from '@server/helpers/uuid'
-import {
-  cleanupTests,
-  updateMyUser,
-  updateVideo,
-  updateVideoChannel,
-  uploadRandomVideoOnServers,
-  wait
-} from '../../../../shared/extra-utils'
-import { ServerInfo } from '../../../../shared/extra-utils/index'
-import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
 import {
   CheckerBaseParams,
   checkMyVideoImportIsFinished,
   checkNewActorFollow,
   checkNewVideoFromSubscription,
   checkVideoIsPublished,
-  getLastNotification,
-  prepareNotificationsTest
-} from '../../../../shared/extra-utils/users/user-notifications'
-import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions'
-import { getBadVideoUrl, getGoodVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
-import { UserNotification, UserNotificationType } from '../../../../shared/models/users'
-import { VideoPrivacy } from '../../../../shared/models/videos'
+  cleanupTests,
+  FIXTURE_URLS,
+  MockSmtpServer,
+  PeerTubeServer,
+  prepareNotificationsTest,
+  uploadRandomVideoOnServers,
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { UserNotification, UserNotificationType, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test user notifications', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let userAccessToken: string
   let userNotifications: UserNotification[] = []
   let adminNotifications: UserNotification[] = []
@@ -69,7 +61,7 @@ describe('Test user notifications', function () {
 
       await uploadRandomVideoOnServers(servers, 1)
 
-      const notification = await getLastNotification(servers[0].url, userAccessToken)
+      const notification = await servers[0].notifications.getLastest({ token: userAccessToken })
       expect(notification).to.be.undefined
 
       expect(emails).to.have.lengthOf(0)
@@ -79,21 +71,21 @@ describe('Test user notifications', function () {
     it('Should send a new video notification if the user follows the local video publisher', async function () {
       this.timeout(15000)
 
-      await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:' + servers[0].port)
+      await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@localhost:' + servers[0].port })
       await waitJobs(servers)
 
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 1)
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence')
+      const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1)
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
     })
 
     it('Should send a new video notification from a remote account', async function () {
       this.timeout(150000) // Server 2 has transcoding enabled
 
-      await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:' + servers[1].port)
+      await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@localhost:' + servers[1].port })
       await waitJobs(servers)
 
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 2)
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence')
+      const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2)
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
     })
 
     it('Should send a new video notification on a scheduled publication', async function () {
@@ -106,13 +98,13 @@ describe('Test user notifications', function () {
         privacy: VideoPrivacy.PRIVATE,
         scheduleUpdate: {
           updateAt: updateAt.toISOString(),
-          privacy: VideoPrivacy.PUBLIC
+          privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC
         }
       }
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 1, data)
+      const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
 
       await wait(6000)
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
     })
 
     it('Should send a new video notification on a remote scheduled publication', async function () {
@@ -125,14 +117,14 @@ describe('Test user notifications', function () {
         privacy: VideoPrivacy.PRIVATE,
         scheduleUpdate: {
           updateAt: updateAt.toISOString(),
-          privacy: VideoPrivacy.PUBLIC
+          privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC
         }
       }
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 2, data)
+      const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
       await waitJobs(servers)
 
       await wait(6000)
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
     })
 
     it('Should not send a notification before the video is published', async function () {
@@ -144,64 +136,64 @@ describe('Test user notifications', function () {
         privacy: VideoPrivacy.PRIVATE,
         scheduleUpdate: {
           updateAt: updateAt.toISOString(),
-          privacy: VideoPrivacy.PUBLIC
+          privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC
         }
       }
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 1, data)
+      const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
 
       await wait(6000)
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
     })
 
     it('Should send a new video notification when a video becomes public', async function () {
       this.timeout(50000)
 
       const data = { privacy: VideoPrivacy.PRIVATE }
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 1, data)
+      const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
 
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
 
-      await updateVideo(servers[0].url, servers[0].accessToken, uuid, { privacy: VideoPrivacy.PUBLIC })
+      await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
 
       await waitJobs(servers)
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
     })
 
     it('Should send a new video notification when a remote video becomes public', async function () {
       this.timeout(50000)
 
       const data = { privacy: VideoPrivacy.PRIVATE }
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 2, data)
+      const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
 
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
 
-      await updateVideo(servers[1].url, servers[1].accessToken, uuid, { privacy: VideoPrivacy.PUBLIC })
+      await servers[1].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
 
       await waitJobs(servers)
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
     })
 
     it('Should not send a new video notification when a video becomes unlisted', async function () {
       this.timeout(50000)
 
       const data = { privacy: VideoPrivacy.PRIVATE }
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 1, data)
+      const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
 
-      await updateVideo(servers[0].url, servers[0].accessToken, uuid, { privacy: VideoPrivacy.UNLISTED })
+      await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } })
 
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
     })
 
     it('Should not send a new video notification when a remote video becomes unlisted', async function () {
       this.timeout(50000)
 
       const data = { privacy: VideoPrivacy.PRIVATE }
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 2, data)
+      const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
 
-      await updateVideo(servers[1].url, servers[1].accessToken, uuid, { privacy: VideoPrivacy.UNLISTED })
+      await servers[1].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } })
 
       await waitJobs(servers)
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'absence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
     })
 
     it('Should send a new video notification after a video import', async function () {
@@ -213,14 +205,13 @@ describe('Test user notifications', function () {
         name,
         channelId,
         privacy: VideoPrivacy.PUBLIC,
-        targetUrl: getGoodVideoUrl()
+        targetUrl: FIXTURE_URLS.goodVideo
       }
-      const res = await importVideo(servers[0].url, servers[0].accessToken, attributes)
-      const uuid = res.body.video.uuid
+      const { video } = await servers[0].imports.importVideo({ attributes })
 
       await waitJobs(servers)
 
-      await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence')
+      await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' })
     })
   })
 
@@ -239,10 +230,10 @@ describe('Test user notifications', function () {
     it('Should not send a notification if transcoding is not enabled', async function () {
       this.timeout(50000)
 
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 1)
+      const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1)
       await waitJobs(servers)
 
-      await checkVideoIsPublished(baseParams, name, uuid, 'absence')
+      await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
     })
 
     it('Should not send a notification if the wait transcoding is false', async function () {
@@ -251,7 +242,7 @@ describe('Test user notifications', function () {
       await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: false })
       await waitJobs(servers)
 
-      const notification = await getLastNotification(servers[0].url, userAccessToken)
+      const notification = await servers[0].notifications.getLastest({ token: userAccessToken })
       if (notification) {
         expect(notification.type).to.not.equal(UserNotificationType.MY_VIDEO_PUBLISHED)
       }
@@ -260,19 +251,19 @@ describe('Test user notifications', function () {
     it('Should send a notification even if the video is not transcoded in other resolutions', async function () {
       this.timeout(50000)
 
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true, fixture: 'video_short_240p.mp4' })
+      const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true, fixture: 'video_short_240p.mp4' })
       await waitJobs(servers)
 
-      await checkVideoIsPublished(baseParams, name, uuid, 'presence')
+      await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
     })
 
     it('Should send a notification with a transcoded video', async function () {
       this.timeout(50000)
 
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true })
+      const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true })
       await waitJobs(servers)
 
-      await checkVideoIsPublished(baseParams, name, uuid, 'presence')
+      await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
     })
 
     it('Should send a notification when an imported video is transcoded', async function () {
@@ -284,14 +275,13 @@ describe('Test user notifications', function () {
         name,
         channelId,
         privacy: VideoPrivacy.PUBLIC,
-        targetUrl: getGoodVideoUrl(),
+        targetUrl: FIXTURE_URLS.goodVideo,
         waitTranscoding: true
       }
-      const res = await importVideo(servers[1].url, servers[1].accessToken, attributes)
-      const uuid = res.body.video.uuid
+      const { video } = await servers[1].imports.importVideo({ attributes })
 
       await waitJobs(servers)
-      await checkVideoIsPublished(baseParams, name, uuid, 'presence')
+      await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' })
     })
 
     it('Should send a notification when the scheduled update has been proceeded', async function () {
@@ -304,13 +294,13 @@ describe('Test user notifications', function () {
         privacy: VideoPrivacy.PRIVATE,
         scheduleUpdate: {
           updateAt: updateAt.toISOString(),
-          privacy: VideoPrivacy.PUBLIC
+          privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC
         }
       }
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 2, data)
+      const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
 
       await wait(6000)
-      await checkVideoIsPublished(baseParams, name, uuid, 'presence')
+      await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
     })
 
     it('Should not send a notification before the video is published', async function () {
@@ -322,13 +312,13 @@ describe('Test user notifications', function () {
         privacy: VideoPrivacy.PRIVATE,
         scheduleUpdate: {
           updateAt: updateAt.toISOString(),
-          privacy: VideoPrivacy.PUBLIC
+          privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC
         }
       }
-      const { name, uuid } = await uploadRandomVideoOnServers(servers, 2, data)
+      const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
 
       await wait(6000)
-      await checkVideoIsPublished(baseParams, name, uuid, 'absence')
+      await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
     })
   })
 
@@ -353,13 +343,14 @@ describe('Test user notifications', function () {
         name,
         channelId,
         privacy: VideoPrivacy.PRIVATE,
-        targetUrl: getBadVideoUrl()
+        targetUrl: FIXTURE_URLS.badVideo
       }
-      const res = await importVideo(servers[0].url, servers[0].accessToken, attributes)
-      const uuid = res.body.video.uuid
+      const { video: { shortUUID } } = await servers[0].imports.importVideo({ attributes })
 
       await waitJobs(servers)
-      await checkMyVideoImportIsFinished(baseParams, name, uuid, getBadVideoUrl(), false, 'presence')
+
+      const url = FIXTURE_URLS.badVideo
+      await checkMyVideoImportIsFinished({ ...baseParams, videoName: name, shortUUID, url, success: false, checkType: 'presence' })
     })
 
     it('Should send a notification when the video import succeeded', async function () {
@@ -371,13 +362,14 @@ describe('Test user notifications', function () {
         name,
         channelId,
         privacy: VideoPrivacy.PRIVATE,
-        targetUrl: getGoodVideoUrl()
+        targetUrl: FIXTURE_URLS.goodVideo
       }
-      const res = await importVideo(servers[0].url, servers[0].accessToken, attributes)
-      const uuid = res.body.video.uuid
+      const { video: { shortUUID } } = await servers[0].imports.importVideo({ attributes })
 
       await waitJobs(servers)
-      await checkMyVideoImportIsFinished(baseParams, name, uuid, getGoodVideoUrl(), true, 'presence')
+
+      const url = FIXTURE_URLS.goodVideo
+      await checkMyVideoImportIsFinished({ ...baseParams, videoName: name, shortUUID, url, success: true, checkType: 'presence' })
     })
   })
 
@@ -394,47 +386,56 @@ describe('Test user notifications', function () {
         token: userAccessToken
       }
 
-      await updateMyUser({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
-        displayName: 'super root name'
-      })
+      await servers[0].users.updateMe({ displayName: 'super root name' })
 
-      await updateMyUser({
-        url: servers[0].url,
-        accessToken: userAccessToken,
+      await servers[0].users.updateMe({
+        token: userAccessToken,
         displayName: myUserName
       })
 
-      await updateMyUser({
-        url: servers[1].url,
-        accessToken: servers[1].accessToken,
-        displayName: 'super root 2 name'
-      })
+      await servers[1].users.updateMe({ displayName: 'super root 2 name' })
 
-      await updateVideoChannel(servers[0].url, userAccessToken, 'user_1_channel', { displayName: myChannelName })
+      await servers[0].channels.update({
+        token: userAccessToken,
+        channelName: 'user_1_channel',
+        attributes: { displayName: myChannelName }
+      })
     })
 
     it('Should notify when a local channel is following one of our channel', async function () {
       this.timeout(50000)
 
-      await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:' + servers[0].port)
+      await servers[0].subscriptions.add({ targetUri: 'user_1_channel@localhost:' + servers[0].port })
       await waitJobs(servers)
 
-      await checkNewActorFollow(baseParams, 'channel', 'root', 'super root name', myChannelName, 'presence')
+      await checkNewActorFollow({
+        ...baseParams,
+        followType: 'channel',
+        followerName: 'root',
+        followerDisplayName: 'super root name',
+        followingDisplayName: myChannelName,
+        checkType: 'presence'
+      })
 
-      await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:' + servers[0].port)
+      await servers[0].subscriptions.remove({ uri: 'user_1_channel@localhost:' + servers[0].port })
     })
 
     it('Should notify when a remote channel is following one of our channel', async function () {
       this.timeout(50000)
 
-      await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:' + servers[0].port)
+      await servers[1].subscriptions.add({ targetUri: 'user_1_channel@localhost:' + servers[0].port })
       await waitJobs(servers)
 
-      await checkNewActorFollow(baseParams, 'channel', 'root', 'super root 2 name', myChannelName, 'presence')
+      await checkNewActorFollow({
+        ...baseParams,
+        followType: 'channel',
+        followerName: 'root',
+        followerDisplayName: 'super root 2 name',
+        followingDisplayName: myChannelName,
+        checkType: 'presence'
+      })
 
-      await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:' + servers[0].port)
+      await servers[1].subscriptions.remove({ uri: 'user_1_channel@localhost:' + servers[0].port })
     })
 
     // PeerTube does not support accout -> account follows
index 4253124c8f1fbedb86efe8719fe6b8ea6bb471e1..5fd464dedbbe7442b5320e912d39795225b28fad 100644 (file)
@@ -1,32 +1,30 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import {
   cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getLocalIdByUUID,
-  ServerInfo,
+  PeerTubeServer,
+  RedundancyCommand,
   setAccessTokensToServers,
-  uploadVideo,
-  uploadVideoAndGetId,
-  waitUntilLog
-} from '../../../../shared/extra-utils'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy, updateRedundancy } from '@shared/extra-utils/server/redundancy'
-import { VideoPrivacy, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
+  waitJobs
+} from '@shared/extra-utils'
+import { VideoPrivacy, VideoRedundanciesTarget } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test manage videos redundancy', function () {
   const targets: VideoRedundanciesTarget[] = [ 'my-videos', 'remote-videos' ]
 
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let video1Server2UUID: string
   let video2Server2UUID: string
   let redundanciesToRemove: number[] = []
 
+  let commands: RedundancyCommand[]
+
   before(async function () {
     this.timeout(120000)
 
@@ -50,40 +48,38 @@ describe('Test manage videos redundancy', function () {
         }
       }
     }
-    servers = await flushAndRunMultipleServers(3, config)
+    servers = await createMultipleServers(3, config)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
 
+    commands = servers.map(s => s.redundancy)
+
     {
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 server 2' })
-      video1Server2UUID = res.body.video.uuid
+      const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
+      video1Server2UUID = uuid
     }
 
     {
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 2 server 2' })
-      video2Server2UUID = res.body.video.uuid
+      const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 2 server 2' } })
+      video2Server2UUID = uuid
     }
 
     await waitJobs(servers)
 
     // Server 1 and server 2 follow each other
     await doubleFollow(servers[0], servers[1])
-    await updateRedundancy(servers[0].url, servers[0].accessToken, servers[1].host, true)
+    await commands[0].updateRedundancy({ host: servers[1].host, redundancyAllowed: true })
 
     await waitJobs(servers)
   })
 
   it('Should not have redundancies on server 3', async function () {
     for (const target of targets) {
-      const res = await listVideoRedundancies({
-        url: servers[2].url,
-        accessToken: servers[2].accessToken,
-        target
-      })
+      const body = await commands[2].listVideos({ target })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
     }
   })
 
@@ -91,31 +87,22 @@ describe('Test manage videos redundancy', function () {
     this.timeout(120000)
 
     await waitJobs(servers)
-    await waitUntilLog(servers[0], 'Duplicated ', 10)
+    await servers[0].servers.waitUntilLog('Duplicated ', 10)
     await waitJobs(servers)
 
-    const res = await listVideoRedundancies({
-      url: servers[1].url,
-      accessToken: servers[1].accessToken,
-      target: 'remote-videos'
-    })
+    const body = await commands[1].listVideos({ target: 'remote-videos' })
 
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   it('Should have "my-videos" redundancies on server 2', async function () {
     this.timeout(120000)
 
-    const res = await listVideoRedundancies({
-      url: servers[1].url,
-      accessToken: servers[1].accessToken,
-      target: 'my-videos'
-    })
-
-    expect(res.body.total).to.equal(2)
+    const body = await commands[1].listVideos({ target: 'my-videos' })
+    expect(body.total).to.equal(2)
 
-    const videos = res.body.data as VideoRedundancy[]
+    const videos = body.data
     expect(videos).to.have.lengthOf(2)
 
     const videos1 = videos.find(v => v.uuid === video1Server2UUID)
@@ -139,28 +126,19 @@ describe('Test manage videos redundancy', function () {
   })
 
   it('Should not have "my-videos" redundancies on server 1', async function () {
-    const res = await listVideoRedundancies({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      target: 'my-videos'
-    })
+    const body = await commands[0].listVideos({ target: 'my-videos' })
 
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   it('Should have "remote-videos" redundancies on server 1', async function () {
     this.timeout(120000)
 
-    const res = await listVideoRedundancies({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      target: 'remote-videos'
-    })
+    const body = await commands[0].listVideos({ target: 'remote-videos' })
+    expect(body.total).to.equal(2)
 
-    expect(res.body.total).to.equal(2)
-
-    const videos = res.body.data as VideoRedundancy[]
+    const videos = body.data
     expect(videos).to.have.lengthOf(2)
 
     const videos1 = videos.find(v => v.uuid === video1Server2UUID)
@@ -185,81 +163,67 @@ describe('Test manage videos redundancy', function () {
 
   it('Should correctly paginate and sort results', async function () {
     {
-      const res = await listVideoRedundancies({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
+      const body = await commands[0].listVideos({
         target: 'remote-videos',
         sort: 'name',
         start: 0,
         count: 2
       })
 
-      const videos = res.body.data
+      const videos = body.data
       expect(videos[0].name).to.equal('video 1 server 2')
       expect(videos[1].name).to.equal('video 2 server 2')
     }
 
     {
-      const res = await listVideoRedundancies({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
+      const body = await commands[0].listVideos({
         target: 'remote-videos',
         sort: '-name',
         start: 0,
         count: 2
       })
 
-      const videos = res.body.data
+      const videos = body.data
       expect(videos[0].name).to.equal('video 2 server 2')
       expect(videos[1].name).to.equal('video 1 server 2')
     }
 
     {
-      const res = await listVideoRedundancies({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
+      const body = await commands[0].listVideos({
         target: 'remote-videos',
         sort: '-name',
         start: 1,
         count: 1
       })
 
-      const videos = res.body.data
-      expect(videos[0].name).to.equal('video 1 server 2')
+      expect(body.data[0].name).to.equal('video 1 server 2')
     }
   })
 
   it('Should manually add a redundancy and list it', async function () {
     this.timeout(120000)
 
-    const uuid = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 3 server 2', privacy: VideoPrivacy.UNLISTED })).uuid
+    const uuid = (await servers[1].videos.quickUpload({ name: 'video 3 server 2', privacy: VideoPrivacy.UNLISTED })).uuid
     await waitJobs(servers)
-    const videoId = await getLocalIdByUUID(servers[0].url, uuid)
+    const videoId = await servers[0].videos.getId({ uuid })
 
-    await addVideoRedundancy({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      videoId
-    })
+    await commands[0].addVideo({ videoId })
 
     await waitJobs(servers)
-    await waitUntilLog(servers[0], 'Duplicated ', 15)
+    await servers[0].servers.waitUntilLog('Duplicated ', 15)
     await waitJobs(servers)
 
     {
-      const res = await listVideoRedundancies({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
+      const body = await commands[0].listVideos({
         target: 'remote-videos',
         sort: '-name',
         start: 0,
         count: 5
       })
 
-      const videos = res.body.data
-      expect(videos[0].name).to.equal('video 3 server 2')
+      const video = body.data[0]
 
-      const video = videos[0]
+      expect(video.name).to.equal('video 3 server 2')
       expect(video.redundancies.files).to.have.lengthOf(4)
       expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
 
@@ -276,19 +240,15 @@ describe('Test manage videos redundancy', function () {
       }
     }
 
-    const res = await listVideoRedundancies({
-      url: servers[1].url,
-      accessToken: servers[1].accessToken,
+    const body = await commands[1].listVideos({
       target: 'my-videos',
       sort: '-name',
       start: 0,
       count: 5
     })
 
-    const videos = res.body.data
-    expect(videos[0].name).to.equal('video 3 server 2')
-
-    const video = videos[0]
+    const video = body.data[0]
+    expect(video.name).to.equal('video 3 server 2')
     expect(video.redundancies.files).to.have.lengthOf(4)
     expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
 
@@ -307,64 +267,47 @@ describe('Test manage videos redundancy', function () {
     this.timeout(120000)
 
     for (const redundancyId of redundanciesToRemove) {
-      await removeVideoRedundancy({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
-        redundancyId
-      })
+      await commands[0].removeVideo({ redundancyId })
     }
 
     {
-      const res = await listVideoRedundancies({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
+      const body = await commands[0].listVideos({
         target: 'remote-videos',
         sort: '-name',
         start: 0,
         count: 5
       })
 
-      const videos = res.body.data
-      expect(videos).to.have.lengthOf(2)
+      const videos = body.data
 
-      expect(videos[0].name).to.equal('video 2 server 2')
+      expect(videos).to.have.lengthOf(2)
 
-      redundanciesToRemove = []
       const video = videos[0]
+      expect(video.name).to.equal('video 2 server 2')
       expect(video.redundancies.files).to.have.lengthOf(4)
       expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
 
       const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists)
 
-      for (const r of redundancies) {
-        redundanciesToRemove.push(r.id)
-      }
+      redundanciesToRemove = redundancies.map(r => r.id)
     }
   })
 
   it('Should remove another (auto) redundancy', async function () {
-    {
-      for (const redundancyId of redundanciesToRemove) {
-        await removeVideoRedundancy({
-          url: servers[0].url,
-          accessToken: servers[0].accessToken,
-          redundancyId
-        })
-      }
+    for (const redundancyId of redundanciesToRemove) {
+      await commands[0].removeVideo({ redundancyId })
+    }
 
-      const res = await listVideoRedundancies({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
-        target: 'remote-videos',
-        sort: '-name',
-        start: 0,
-        count: 5
-      })
+    const body = await commands[0].listVideos({
+      target: 'remote-videos',
+      sort: '-name',
+      start: 0,
+      count: 5
+    })
 
-      const videos = res.body.data
-      expect(videos[0].name).to.equal('video 1 server 2')
-      expect(videos).to.have.lengthOf(1)
-    }
+    const videos = body.data
+    expect(videos).to.have.lengthOf(1)
+    expect(videos[0].name).to.equal('video 1 server 2')
   })
 
   after(async function () {
index 1cb1603bc0a3989bb958bc9731bfb0ccd1f16204..933a2c7760c1f881f9582f0c7127f7ba40b2d983 100644 (file)
@@ -1,29 +1,14 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import * as chai from 'chai'
-import { listVideoRedundancies, updateRedundancy } from '@shared/extra-utils/server/redundancy'
+import { expect } from 'chai'
+import { cleanupTests, createSingleServer, killallServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
 import { VideoPrivacy } from '@shared/models'
-import {
-  cleanupTests,
-  flushAndRunServer,
-  follow,
-  killallServers,
-  reRunServer,
-  ServerInfo,
-  setAccessTokensToServers,
-  updateVideo,
-  uploadVideo,
-  waitUntilLog
-} from '../../../../shared/extra-utils'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-
-const expect = chai.expect
 
 describe('Test redundancy constraints', function () {
-  let remoteServer: ServerInfo
-  let localServer: ServerInfo
-  let servers: ServerInfo[]
+  let remoteServer: PeerTubeServer
+  let localServer: PeerTubeServer
+  let servers: PeerTubeServer[]
 
   const remoteServerConfig = {
     redundancy: {
@@ -43,38 +28,30 @@ describe('Test redundancy constraints', function () {
 
   async function uploadWrapper (videoName: string) {
     // Wait for transcoding
-    const res = await uploadVideo(localServer.url, localServer.accessToken, { name: 'to transcode', privacy: VideoPrivacy.PRIVATE })
+    const { id } = await localServer.videos.upload({ attributes: { name: 'to transcode', privacy: VideoPrivacy.PRIVATE } })
     await waitJobs([ localServer ])
 
     // Update video to schedule a federation
-    await updateVideo(localServer.url, localServer.accessToken, res.body.video.id, { name: videoName, privacy: VideoPrivacy.PUBLIC })
+    await localServer.videos.update({ id, attributes: { name: videoName, privacy: VideoPrivacy.PUBLIC } })
   }
 
   async function getTotalRedundanciesLocalServer () {
-    const res = await listVideoRedundancies({
-      url: localServer.url,
-      accessToken: localServer.accessToken,
-      target: 'my-videos'
-    })
+    const body = await localServer.redundancy.listVideos({ target: 'my-videos' })
 
-    return res.body.total
+    return body.total
   }
 
   async function getTotalRedundanciesRemoteServer () {
-    const res = await listVideoRedundancies({
-      url: remoteServer.url,
-      accessToken: remoteServer.accessToken,
-      target: 'remote-videos'
-    })
+    const body = await remoteServer.redundancy.listVideos({ target: 'remote-videos' })
 
-    return res.body.total
+    return body.total
   }
 
   before(async function () {
     this.timeout(120000)
 
     {
-      remoteServer = await flushAndRunServer(1, remoteServerConfig)
+      remoteServer = await createSingleServer(1, remoteServerConfig)
     }
 
     {
@@ -85,7 +62,7 @@ describe('Test redundancy constraints', function () {
           }
         }
       }
-      localServer = await flushAndRunServer(2, config)
+      localServer = await createSingleServer(2, config)
     }
 
     servers = [ remoteServer, localServer ]
@@ -93,14 +70,14 @@ describe('Test redundancy constraints', function () {
     // Get the access tokens
     await setAccessTokensToServers(servers)
 
-    await uploadVideo(localServer.url, localServer.accessToken, { name: 'video 1 server 2' })
+    await localServer.videos.upload({ attributes: { name: 'video 1 server 2' } })
 
     await waitJobs(servers)
 
     // Server 1 and server 2 follow each other
-    await follow(remoteServer.url, [ localServer.url ], remoteServer.accessToken)
+    await remoteServer.follows.follow({ hosts: [ localServer.url ] })
     await waitJobs(servers)
-    await updateRedundancy(remoteServer.url, remoteServer.accessToken, localServer.host, true)
+    await remoteServer.redundancy.updateRedundancy({ host: localServer.host, redundancyAllowed: true })
 
     await waitJobs(servers)
   })
@@ -109,7 +86,7 @@ describe('Test redundancy constraints', function () {
     this.timeout(120000)
 
     await waitJobs(servers)
-    await waitUntilLog(remoteServer, 'Duplicated ', 5)
+    await remoteServer.servers.waitUntilLog('Duplicated ', 5)
     await waitJobs(servers)
 
     {
@@ -134,11 +111,11 @@ describe('Test redundancy constraints', function () {
       }
     }
     await killallServers([ localServer ])
-    await reRunServer(localServer, config)
+    await localServer.run(config)
 
     await uploadWrapper('video 2 server 2')
 
-    await waitUntilLog(remoteServer, 'Duplicated ', 10)
+    await remoteServer.servers.waitUntilLog('Duplicated ', 10)
     await waitJobs(servers)
 
     {
@@ -163,11 +140,11 @@ describe('Test redundancy constraints', function () {
       }
     }
     await killallServers([ localServer ])
-    await reRunServer(localServer, config)
+    await localServer.run(config)
 
     await uploadWrapper('video 3 server 2')
 
-    await waitUntilLog(remoteServer, 'Duplicated ', 15)
+    await remoteServer.servers.waitUntilLog('Duplicated ', 15)
     await waitJobs(servers)
 
     {
@@ -184,11 +161,11 @@ describe('Test redundancy constraints', function () {
   it('Should have redundancy on server 1 and on server 2 with followings filter now server 2 follows server 1', async function () {
     this.timeout(120000)
 
-    await follow(localServer.url, [ remoteServer.url ], localServer.accessToken)
+    await localServer.follows.follow({ hosts: [ remoteServer.url ] })
     await waitJobs(servers)
 
     await uploadWrapper('video 4 server 2')
-    await waitUntilLog(remoteServer, 'Duplicated ', 20)
+    await remoteServer.servers.waitUntilLog('Duplicated ', 20)
     await waitJobs(servers)
 
     {
index 0e0a73b9ddc3b9a0063fdd5a4178b07616d0c98b..e1a12f5f8c521be2884dac9ec1f8b4e2c4a2386e 100644 (file)
@@ -4,72 +4,63 @@ import 'mocha'
 import * as chai from 'chai'
 import { readdir } from 'fs-extra'
 import * as magnetUtil from 'magnet-uri'
-import { join } from 'path'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { basename, join } from 'path'
 import {
   checkSegmentHash,
   checkVideoFilesWereRemoved,
   cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getFollowingListPaginationAndSort,
-  getVideo,
-  getVideoWithToken,
-  immutableAssign,
   killallServers,
-  makeGetRequest,
-  removeVideo,
-  reRunServer,
+  makeRawRequest,
+  PeerTubeServer,
   root,
-  ServerInfo,
+  saveVideoInServers,
   setAccessTokensToServers,
-  unfollow,
-  updateVideo,
-  uploadVideo,
-  viewVideo,
   wait,
-  waitUntilLog
-} from '../../../../shared/extra-utils'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
+  waitJobs
+} from '@shared/extra-utils'
 import {
-  addVideoRedundancy,
-  listVideoRedundancies,
-  removeVideoRedundancy,
-  updateRedundancy
-} from '../../../../shared/extra-utils/server/redundancy'
-import { getStats } from '../../../../shared/extra-utils/server/stats'
-import { ActorFollow } from '../../../../shared/models/actors'
-import { VideoRedundancy, VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '../../../../shared/models/redundancy'
-import { ServerStats } from '../../../../shared/models/server/server-stats.model'
-import { VideoDetails, VideoPrivacy } from '../../../../shared/models/videos'
+  HttpStatusCode,
+  VideoDetails,
+  VideoFile,
+  VideoPrivacy,
+  VideoRedundancyStrategy,
+  VideoRedundancyStrategyWithManual
+} from '@shared/models'
 
 const expect = chai.expect
 
-let servers: ServerInfo[] = []
-let video1Server2UUID: string
-let video1Server2Id: number
+let servers: PeerTubeServer[] = []
+let video1Server2: VideoDetails
 
-function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) {
+async function checkMagnetWebseeds (file: VideoFile, baseWebseeds: string[], server: PeerTubeServer) {
   const parsed = magnetUtil.decode(file.magnetUri)
 
   for (const ws of baseWebseeds) {
-    const found = parsed.urlList.find(url => url === `${ws}-${file.resolution.id}.mp4`)
+    const found = parsed.urlList.find(url => url === `${ws}${basename(file.fileUrl)}`)
     expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined
   }
 
   expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
+
+  for (const url of parsed.urlList) {
+    await makeRawRequest(url, HttpStatusCode.OK_200)
+  }
 }
 
-async function flushAndRunServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebtorrent = true) {
+async function createSingleServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebtorrent = true) {
   const strategies: any[] = []
 
   if (strategy !== null) {
     strategies.push(
-      immutableAssign({
+      {
         min_lifetime: '1 hour',
         strategy: strategy,
-        size: '400KB'
-      }, additionalParams)
+        size: '400KB',
+
+        ...additionalParams
+      }
     )
   }
 
@@ -90,17 +81,16 @@ async function flushAndRunServers (strategy: VideoRedundancyStrategy | null, add
     }
   }
 
-  servers = await flushAndRunMultipleServers(3, config)
+  servers = await createMultipleServers(3, config)
 
   // Get the access tokens
   await setAccessTokensToServers(servers)
 
   {
-    const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 server 2' })
-    video1Server2UUID = res.body.video.uuid
-    video1Server2Id = res.body.video.id
+    const { id } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
+    video1Server2 = await servers[1].videos.get({ id })
 
-    await viewVideo(servers[1].url, video1Server2UUID)
+    await servers[1].videos.view({ id })
   }
 
   await waitJobs(servers)
@@ -115,55 +105,65 @@ async function flushAndRunServers (strategy: VideoRedundancyStrategy | null, add
   await waitJobs(servers)
 }
 
+async function ensureSameFilenames (videoUUID: string) {
+  let webtorrentFilenames: string[]
+  let hlsFilenames: string[]
+
+  for (const server of servers) {
+    const video = await server.videos.getWithToken({ id: videoUUID })
+
+    // Ensure we use the same filenames that the origin
+
+    const localWebtorrentFilenames = video.files.map(f => basename(f.fileUrl)).sort()
+    const localHLSFilenames = video.streamingPlaylists[0].files.map(f => basename(f.fileUrl)).sort()
+
+    if (webtorrentFilenames) expect(webtorrentFilenames).to.deep.equal(localWebtorrentFilenames)
+    else webtorrentFilenames = localWebtorrentFilenames
+
+    if (hlsFilenames) expect(hlsFilenames).to.deep.equal(localHLSFilenames)
+    else hlsFilenames = localHLSFilenames
+  }
+
+  return { webtorrentFilenames, hlsFilenames }
+}
+
 async function check1WebSeed (videoUUID?: string) {
-  if (!videoUUID) videoUUID = video1Server2UUID
+  if (!videoUUID) videoUUID = video1Server2.uuid
 
   const webseeds = [
-    `http://localhost:${servers[1].port}/static/webseed/${videoUUID}`
+    `http://localhost:${servers[1].port}/static/webseed/`
   ]
 
   for (const server of servers) {
     // With token to avoid issues with video follow constraints
-    const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
+    const video = await server.videos.getWithToken({ id: videoUUID })
 
-    const video: VideoDetails = res.body
     for (const f of video.files) {
-      checkMagnetWebseeds(f, webseeds, server)
+      await checkMagnetWebseeds(f, webseeds, server)
     }
   }
+
+  await ensureSameFilenames(videoUUID)
 }
 
 async function check2Webseeds (videoUUID?: string) {
-  if (!videoUUID) videoUUID = video1Server2UUID
+  if (!videoUUID) videoUUID = video1Server2.uuid
 
   const webseeds = [
-    `http://localhost:${servers[0].port}/static/redundancy/${videoUUID}`,
-    `http://localhost:${servers[1].port}/static/webseed/${videoUUID}`
+    `http://localhost:${servers[0].port}/static/redundancy/`,
+    `http://localhost:${servers[1].port}/static/webseed/`
   ]
 
   for (const server of servers) {
-    const res = await getVideo(server.url, videoUUID)
-
-    const video: VideoDetails = res.body
+    const video = await server.videos.get({ id: videoUUID })
 
     for (const file of video.files) {
-      checkMagnetWebseeds(file, webseeds, server)
-
-      await makeGetRequest({
-        url: servers[0].url,
-        statusCodeExpected: HttpStatusCode.OK_200,
-        path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`,
-        contentType: null
-      })
-      await makeGetRequest({
-        url: servers[1].url,
-        statusCodeExpected: HttpStatusCode.OK_200,
-        path: `/static/webseed/${videoUUID}-${file.resolution.id}.mp4`,
-        contentType: null
-      })
+      await checkMagnetWebseeds(file, webseeds, server)
     }
   }
 
+  const { webtorrentFilenames } = await ensureSameFilenames(videoUUID)
+
   const directories = [
     'test' + servers[0].internalServerNumber + '/redundancy',
     'test' + servers[1].internalServerNumber + '/videos'
@@ -173,32 +173,31 @@ async function check2Webseeds (videoUUID?: string) {
     const files = await readdir(join(root(), directory))
     expect(files).to.have.length.at.least(4)
 
-    for (const resolution of [ 240, 360, 480, 720 ]) {
-      expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined
-    }
+    // Ensure we files exist on disk
+    expect(files.find(f => webtorrentFilenames.includes(f))).to.exist
   }
 }
 
 async function check0PlaylistRedundancies (videoUUID?: string) {
-  if (!videoUUID) videoUUID = video1Server2UUID
+  if (!videoUUID) videoUUID = video1Server2.uuid
 
   for (const server of servers) {
     // With token to avoid issues with video follow constraints
-    const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
-    const video: VideoDetails = res.body
+    const video = await server.videos.getWithToken({ id: videoUUID })
 
     expect(video.streamingPlaylists).to.be.an('array')
     expect(video.streamingPlaylists).to.have.lengthOf(1)
     expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0)
   }
+
+  await ensureSameFilenames(videoUUID)
 }
 
 async function check1PlaylistRedundancies (videoUUID?: string) {
-  if (!videoUUID) videoUUID = video1Server2UUID
+  if (!videoUUID) videoUUID = video1Server2.uuid
 
   for (const server of servers) {
-    const res = await getVideo(server.url, videoUUID)
-    const video: VideoDetails = res.body
+    const video = await server.videos.get({ id: videoUUID })
 
     expect(video.streamingPlaylists).to.have.lengthOf(1)
     expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1)
@@ -211,13 +210,15 @@ async function check1PlaylistRedundancies (videoUUID?: string) {
   const baseUrlPlaylist = servers[1].url + '/static/streaming-playlists/hls'
   const baseUrlSegment = servers[0].url + '/static/redundancy/hls'
 
-  const res = await getVideo(servers[0].url, videoUUID)
-  const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
+  const video = await servers[0].videos.get({ id: videoUUID })
+  const hlsPlaylist = video.streamingPlaylists[0]
 
   for (const resolution of [ 240, 360, 480, 720 ]) {
-    await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
+    await checkSegmentHash({ server: servers[1], baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist })
   }
 
+  const { hlsFilenames } = await ensureSameFilenames(videoUUID)
+
   const directories = [
     'test' + servers[0].internalServerNumber + '/redundancy/hls',
     'test' + servers[1].internalServerNumber + '/streaming-playlists/hls'
@@ -227,11 +228,8 @@ async function check1PlaylistRedundancies (videoUUID?: string) {
     const files = await readdir(join(root(), directory, videoUUID))
     expect(files).to.have.length.at.least(4)
 
-    for (const resolution of [ 240, 360, 480, 720 ]) {
-      const filename = `${videoUUID}-${resolution}-fragmented.mp4`
-
-      expect(files.find(f => f === filename)).to.not.be.undefined
-    }
+    // Ensure we files exist on disk
+    expect(files.find(f => hlsFilenames.includes(f))).to.exist
   }
 }
 
@@ -244,9 +242,7 @@ async function checkStatsGlobal (strategy: VideoRedundancyStrategyWithManual) {
     statsLength = 2
   }
 
-  const res = await getStats(servers[0].url)
-  const data: ServerStats = res.body
-
+  const data = await servers[0].stats.get()
   expect(data.videosRedundancy).to.have.lengthOf(statsLength)
 
   const stat = data.videosRedundancy[0]
@@ -272,14 +268,20 @@ async function checkStatsWithoutRedundancy (strategy: VideoRedundancyStrategyWit
   expect(stat.totalVideos).to.equal(0)
 }
 
-async function enableRedundancyOnServer1 () {
-  await updateRedundancy(servers[0].url, servers[0].accessToken, servers[1].host, true)
-
-  const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: '-createdAt' })
-  const follows: ActorFollow[] = res.body.data
+async function findServerFollows () {
+  const body = await servers[0].follows.getFollowings({ start: 0, count: 5, sort: '-createdAt' })
+  const follows = body.data
   const server2 = follows.find(f => f.following.host === `localhost:${servers[1].port}`)
   const server3 = follows.find(f => f.following.host === `localhost:${servers[2].port}`)
 
+  return { server2, server3 }
+}
+
+async function enableRedundancyOnServer1 () {
+  await servers[0].redundancy.updateRedundancy({ host: servers[1].host, redundancyAllowed: true })
+
+  const { server2, server3 } = await findServerFollows()
+
   expect(server3).to.not.be.undefined
   expect(server3.following.hostRedundancyAllowed).to.be.false
 
@@ -288,12 +290,9 @@ async function enableRedundancyOnServer1 () {
 }
 
 async function disableRedundancyOnServer1 () {
-  await updateRedundancy(servers[0].url, servers[0].accessToken, servers[1].host, false)
+  await servers[0].redundancy.updateRedundancy({ host: servers[1].host, redundancyAllowed: false })
 
-  const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: '-createdAt' })
-  const follows: ActorFollow[] = res.body.data
-  const server2 = follows.find(f => f.following.host === `localhost:${servers[1].port}`)
-  const server3 = follows.find(f => f.following.host === `localhost:${servers[2].port}`)
+  const { server2, server3 } = await findServerFollows()
 
   expect(server3).to.not.be.undefined
   expect(server3.following.hostRedundancyAllowed).to.be.false
@@ -310,7 +309,7 @@ describe('Test videos redundancy', function () {
     before(function () {
       this.timeout(120000)
 
-      return flushAndRunServers(strategy)
+      return createSingleServers(strategy)
     })
 
     it('Should have 1 webseed on the first video', async function () {
@@ -327,7 +326,7 @@ describe('Test videos redundancy', function () {
       this.timeout(80000)
 
       await waitJobs(servers)
-      await waitUntilLog(servers[0], 'Duplicated ', 5)
+      await servers[0].servers.waitUntilLog('Duplicated ', 5)
       await waitJobs(servers)
 
       await check2Webseeds()
@@ -346,7 +345,7 @@ describe('Test videos redundancy', function () {
       await check1WebSeed()
       await check0PlaylistRedundancies()
 
-      await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].internalServerNumber, [ 'videos', join('playlists', 'hls') ])
+      await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
     })
 
     after(async function () {
@@ -360,7 +359,7 @@ describe('Test videos redundancy', function () {
     before(function () {
       this.timeout(120000)
 
-      return flushAndRunServers(strategy)
+      return createSingleServers(strategy)
     })
 
     it('Should have 1 webseed on the first video', async function () {
@@ -377,7 +376,7 @@ describe('Test videos redundancy', function () {
       this.timeout(80000)
 
       await waitJobs(servers)
-      await waitUntilLog(servers[0], 'Duplicated ', 5)
+      await servers[0].servers.waitUntilLog('Duplicated ', 5)
       await waitJobs(servers)
 
       await check2Webseeds()
@@ -388,7 +387,7 @@ describe('Test videos redundancy', function () {
     it('Should unfollow on server 1 and remove duplicated videos', async function () {
       this.timeout(80000)
 
-      await unfollow(servers[0].url, servers[0].accessToken, servers[1])
+      await servers[0].follows.unfollow({ target: servers[1] })
 
       await waitJobs(servers)
       await wait(5000)
@@ -396,7 +395,7 @@ describe('Test videos redundancy', function () {
       await check1WebSeed()
       await check0PlaylistRedundancies()
 
-      await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].internalServerNumber, [ 'videos' ])
+      await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
     })
 
     after(async function () {
@@ -410,7 +409,7 @@ describe('Test videos redundancy', function () {
     before(function () {
       this.timeout(120000)
 
-      return flushAndRunServers(strategy, { min_views: 3 })
+      return createSingleServers(strategy, { min_views: 3 })
     })
 
     it('Should have 1 webseed on the first video', async function () {
@@ -438,8 +437,8 @@ describe('Test videos redundancy', function () {
     it('Should view 2 times the first video to have > min_views config', async function () {
       this.timeout(80000)
 
-      await viewVideo(servers[0].url, video1Server2UUID)
-      await viewVideo(servers[2].url, video1Server2UUID)
+      await servers[0].videos.view({ id: video1Server2.uuid })
+      await servers[2].videos.view({ id: video1Server2.uuid })
 
       await wait(10000)
       await waitJobs(servers)
@@ -449,7 +448,7 @@ describe('Test videos redundancy', function () {
       this.timeout(80000)
 
       await waitJobs(servers)
-      await waitUntilLog(servers[0], 'Duplicated ', 5)
+      await servers[0].servers.waitUntilLog('Duplicated ', 5)
       await waitJobs(servers)
 
       await check2Webseeds()
@@ -460,12 +459,13 @@ describe('Test videos redundancy', function () {
     it('Should remove the video and the redundancy files', async function () {
       this.timeout(20000)
 
-      await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
+      await saveVideoInServers(servers, video1Server2.uuid)
+      await servers[1].videos.remove({ id: video1Server2.uuid })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        await checkVideoFilesWereRemoved(video1Server2UUID, server.internalServerNumber)
+        await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
       }
     })
 
@@ -480,7 +480,7 @@ describe('Test videos redundancy', function () {
     before(async function () {
       this.timeout(120000)
 
-      await flushAndRunServers(strategy, { min_views: 3 }, false)
+      await createSingleServers(strategy, { min_views: 3 }, false)
     })
 
     it('Should have 0 playlist redundancy on the first video', async function () {
@@ -506,14 +506,14 @@ describe('Test videos redundancy', function () {
     it('Should have 1 redundancy on the first video', async function () {
       this.timeout(160000)
 
-      await viewVideo(servers[0].url, video1Server2UUID)
-      await viewVideo(servers[2].url, video1Server2UUID)
+      await servers[0].videos.view({ id: video1Server2.uuid })
+      await servers[2].videos.view({ id: video1Server2.uuid })
 
       await wait(10000)
       await waitJobs(servers)
 
       await waitJobs(servers)
-      await waitUntilLog(servers[0], 'Duplicated ', 1)
+      await servers[0].servers.waitUntilLog('Duplicated ', 1)
       await waitJobs(servers)
 
       await check1PlaylistRedundancies()
@@ -523,12 +523,13 @@ describe('Test videos redundancy', function () {
     it('Should remove the video and the redundancy files', async function () {
       this.timeout(20000)
 
-      await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
+      await saveVideoInServers(servers, video1Server2.uuid)
+      await servers[1].videos.remove({ id: video1Server2.uuid })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        await checkVideoFilesWereRemoved(video1Server2UUID, server.internalServerNumber)
+        await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
       }
     })
 
@@ -541,7 +542,7 @@ describe('Test videos redundancy', function () {
     before(function () {
       this.timeout(120000)
 
-      return flushAndRunServers(null)
+      return createSingleServers(null)
     })
 
     it('Should have 1 webseed on the first video', async function () {
@@ -551,18 +552,14 @@ describe('Test videos redundancy', function () {
     })
 
     it('Should create a redundancy on first video', async function () {
-      await addVideoRedundancy({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
-        videoId: video1Server2Id
-      })
+      await servers[0].redundancy.addVideo({ videoId: video1Server2.id })
     })
 
     it('Should have 2 webseeds on the first video', async function () {
       this.timeout(80000)
 
       await waitJobs(servers)
-      await waitUntilLog(servers[0], 'Duplicated ', 5)
+      await servers[0].servers.waitUntilLog('Duplicated ', 5)
       await waitJobs(servers)
 
       await check2Webseeds()
@@ -573,22 +570,15 @@ describe('Test videos redundancy', function () {
     it('Should manually remove redundancies on server 1 and remove duplicated videos', async function () {
       this.timeout(80000)
 
-      const res = await listVideoRedundancies({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
-        target: 'remote-videos'
-      })
+      const body = await servers[0].redundancy.listVideos({ target: 'remote-videos' })
 
-      const videos = res.body.data as VideoRedundancy[]
+      const videos = body.data
       expect(videos).to.have.lengthOf(1)
 
       const video = videos[0]
+
       for (const r of video.redundancies.files.concat(video.redundancies.streamingPlaylists)) {
-        await removeVideoRedundancy({
-          url: servers[0].url,
-          accessToken: servers[0].accessToken,
-          redundancyId: r.id
-        })
+        await servers[0].redundancy.removeVideo({ redundancyId: r.id })
       }
 
       await waitJobs(servers)
@@ -597,7 +587,7 @@ describe('Test videos redundancy', function () {
       await check1WebSeed()
       await check0PlaylistRedundancies()
 
-      await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
+      await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
     })
 
     after(async function () {
@@ -608,10 +598,9 @@ describe('Test videos redundancy', function () {
   describe('Test expiration', function () {
     const strategy = 'recently-added'
 
-    async function checkContains (servers: ServerInfo[], str: string) {
+    async function checkContains (servers: PeerTubeServer[], str: string) {
       for (const server of servers) {
-        const res = await getVideo(server.url, video1Server2UUID)
-        const video: VideoDetails = res.body
+        const video = await server.videos.get({ id: video1Server2.uuid })
 
         for (const f of video.files) {
           expect(f.magnetUri).to.contain(str)
@@ -619,10 +608,9 @@ describe('Test videos redundancy', function () {
       }
     }
 
-    async function checkNotContains (servers: ServerInfo[], str: string) {
+    async function checkNotContains (servers: PeerTubeServer[], str: string) {
       for (const server of servers) {
-        const res = await getVideo(server.url, video1Server2UUID)
-        const video: VideoDetails = res.body
+        const video = await server.videos.get({ id: video1Server2.uuid })
 
         for (const f of video.files) {
           expect(f.magnetUri).to.not.contain(str)
@@ -633,7 +621,7 @@ describe('Test videos redundancy', function () {
     before(async function () {
       this.timeout(120000)
 
-      await flushAndRunServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
+      await createSingleServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
 
       await enableRedundancyOnServer1()
     })
@@ -656,7 +644,7 @@ describe('Test videos redundancy', function () {
     it('Should stop server 1 and expire video redundancy', async function () {
       this.timeout(80000)
 
-      killallServers([ servers[0] ])
+      await killallServers([ servers[0] ])
 
       await wait(15000)
 
@@ -675,25 +663,25 @@ describe('Test videos redundancy', function () {
     before(async function () {
       this.timeout(120000)
 
-      await flushAndRunServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
+      await createSingleServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
 
       await enableRedundancyOnServer1()
 
       await waitJobs(servers)
-      await waitUntilLog(servers[0], 'Duplicated ', 5)
+      await servers[0].servers.waitUntilLog('Duplicated ', 5)
       await waitJobs(servers)
 
-      await check2Webseeds(video1Server2UUID)
-      await check1PlaylistRedundancies(video1Server2UUID)
+      await check2Webseeds()
+      await check1PlaylistRedundancies()
       await checkStatsWith1Redundancy(strategy)
 
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 2 server 2', privacy: VideoPrivacy.PRIVATE })
-      video2Server2UUID = res.body.video.uuid
+      const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 2 server 2', privacy: VideoPrivacy.PRIVATE } })
+      video2Server2UUID = uuid
 
       // Wait transcoding before federation
       await waitJobs(servers)
 
-      await updateVideo(servers[1].url, servers[1].accessToken, video2Server2UUID, { privacy: VideoPrivacy.PUBLIC })
+      await servers[1].videos.update({ id: video2Server2UUID, attributes: { privacy: VideoPrivacy.PUBLIC } })
     })
 
     it('Should cache video 2 webseeds on the first video', async function () {
@@ -707,8 +695,8 @@ describe('Test videos redundancy', function () {
         await wait(1000)
 
         try {
-          await check1WebSeed(video1Server2UUID)
-          await check0PlaylistRedundancies(video1Server2UUID)
+          await check1WebSeed()
+          await check0PlaylistRedundancies()
 
           await check2Webseeds(video2Server2UUID)
           await check1PlaylistRedundancies(video2Server2UUID)
@@ -725,8 +713,8 @@ describe('Test videos redundancy', function () {
 
       await waitJobs(servers)
 
-      killallServers([ servers[0] ])
-      await reRunServer(servers[0], {
+      await killallServers([ servers[0] ])
+      await servers[0].run({
         redundancy: {
           videos: {
             check_interval: '1 second',
@@ -737,7 +725,7 @@ describe('Test videos redundancy', function () {
 
       await waitJobs(servers)
 
-      await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].internalServerNumber, [ join('redundancy', 'hls') ])
+      await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
     })
 
     after(async function () {
index e83eb717116e89411c372ab10b3f82758824e087..426cbc8e1bec1f61eb4489c2a69d1280b3e344fb 100644 (file)
@@ -1,69 +1,63 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import {
-  addVideoChannel,
   cleanupTests,
-  createUser,
-  deleteVideoChannel,
-  flushAndRunMultipleServers,
-  getVideoChannelsList,
-  getVideoChannelVideos,
-  ServerInfo,
+  createMultipleServers,
+  PeerTubeServer,
+  SearchCommand,
   setAccessTokensToServers,
-  updateMyUser,
-  updateVideo,
-  updateVideoChannel,
-  uploadVideo,
-  userLogin,
-  wait
-} from '../../../../shared/extra-utils'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { VideoChannel } from '../../../../shared/models/videos'
-import { searchVideoChannel } from '../../../../shared/extra-utils/search/video-channels'
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { VideoChannel } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test ActivityPub video channels search', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let userServer2Token: string
   let videoServer2UUID: string
   let channelIdServer2: number
+  let command: SearchCommand
 
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
 
     {
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: 'user1_server1', password: 'password' })
+      await servers[0].users.create({ username: 'user1_server1', password: 'password' })
       const channel = {
         name: 'channel1_server1',
         displayName: 'Channel 1 server 1'
       }
-      await addVideoChannel(servers[0].url, servers[0].accessToken, channel)
+      await servers[0].channels.create({ attributes: channel })
     }
 
     {
       const user = { username: 'user1_server2', password: 'password' }
-      await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
-      userServer2Token = await userLogin(servers[1], user)
+      await servers[1].users.create({ username: user.username, password: user.password })
+      userServer2Token = await servers[1].login.getAccessToken(user)
 
       const channel = {
         name: 'channel1_server2',
         displayName: 'Channel 1 server 2'
       }
-      const resChannel = await addVideoChannel(servers[1].url, userServer2Token, channel)
-      channelIdServer2 = resChannel.body.videoChannel.id
+      const created = await servers[1].channels.create({ token: userServer2Token, attributes: channel })
+      channelIdServer2 = created.id
 
-      const res = await uploadVideo(servers[1].url, userServer2Token, { name: 'video 1 server 2', channelId: channelIdServer2 })
-      videoServer2UUID = res.body.video.uuid
+      const attributes = { name: 'video 1 server 2', channelId: channelIdServer2 }
+      const { uuid } = await servers[1].videos.upload({ token: userServer2Token, attributes })
+      videoServer2UUID = uuid
     }
 
     await waitJobs(servers)
+
+    command = servers[0].search
   })
 
   it('Should not find a remote video channel', async function () {
@@ -71,21 +65,21 @@ describe('Test ActivityPub video channels search', function () {
 
     {
       const search = 'http://localhost:' + servers[1].port + '/video-channels/channel1_server3'
-      const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
+      const body = await command.searchChannels({ search, token: servers[0].accessToken })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     }
 
     {
       // Without token
       const search = 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2'
-      const res = await searchVideoChannel(servers[0].url, search)
+      const body = await command.searchChannels({ search })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     }
   })
 
@@ -96,13 +90,13 @@ describe('Test ActivityPub video channels search', function () {
     ]
 
     for (const search of searches) {
-      const res = await searchVideoChannel(servers[0].url, search)
+      const body = await command.searchChannels({ search })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].name).to.equal('channel1_server1')
-      expect(res.body.data[0].displayName).to.equal('Channel 1 server 1')
+      expect(body.total).to.equal(1)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].name).to.equal('channel1_server1')
+      expect(body.data[0].displayName).to.equal('Channel 1 server 1')
     }
   })
 
@@ -110,13 +104,13 @@ describe('Test ActivityPub video channels search', function () {
     const search = 'http://localhost:' + servers[0].port + '/c/channel1_server1'
 
     for (const token of [ undefined, servers[0].accessToken ]) {
-      const res = await searchVideoChannel(servers[0].url, search, token)
+      const body = await command.searchChannels({ search, token })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].name).to.equal('channel1_server1')
-      expect(res.body.data[0].displayName).to.equal('Channel 1 server 1')
+      expect(body.total).to.equal(1)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].name).to.equal('channel1_server1')
+      expect(body.data[0].displayName).to.equal('Channel 1 server 1')
     }
   })
 
@@ -129,23 +123,23 @@ describe('Test ActivityPub video channels search', function () {
     ]
 
     for (const search of searches) {
-      const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
+      const body = await command.searchChannels({ search, token: servers[0].accessToken })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].name).to.equal('channel1_server2')
-      expect(res.body.data[0].displayName).to.equal('Channel 1 server 2')
+      expect(body.total).to.equal(1)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].name).to.equal('channel1_server2')
+      expect(body.data[0].displayName).to.equal('Channel 1 server 2')
     }
   })
 
   it('Should not list this remote video channel', async function () {
-    const res = await getVideoChannelsList(servers[0].url, 0, 5)
-    expect(res.body.total).to.equal(3)
-    expect(res.body.data).to.have.lengthOf(3)
-    expect(res.body.data[0].name).to.equal('channel1_server1')
-    expect(res.body.data[1].name).to.equal('user1_server1_channel')
-    expect(res.body.data[2].name).to.equal('root_channel')
+    const body = await servers[0].channels.list()
+    expect(body.total).to.equal(3)
+    expect(body.data).to.have.lengthOf(3)
+    expect(body.data[0].name).to.equal('channel1_server1')
+    expect(body.data[1].name).to.equal('user1_server1_channel')
+    expect(body.data[2].name).to.equal('root_channel')
   })
 
   it('Should list video channel videos of server 2 without token', async function () {
@@ -153,34 +147,43 @@ describe('Test ActivityPub video channels search', function () {
 
     await waitJobs(servers)
 
-    const res = await getVideoChannelVideos(servers[0].url, null, 'channel1_server2@localhost:' + servers[1].port, 0, 5)
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    const { total, data } = await servers[0].videos.listByChannel({
+      token: null,
+      handle: 'channel1_server2@localhost:' + servers[1].port
+    })
+    expect(total).to.equal(0)
+    expect(data).to.have.lengthOf(0)
   })
 
   it('Should list video channel videos of server 2 with token', async function () {
-    const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, 'channel1_server2@localhost:' + servers[1].port, 0, 5)
+    const { total, data } = await servers[0].videos.listByChannel({
+      handle: 'channel1_server2@localhost:' + servers[1].port
+    })
 
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data[0].name).to.equal('video 1 server 2')
+    expect(total).to.equal(1)
+    expect(data[0].name).to.equal('video 1 server 2')
   })
 
   it('Should update video channel of server 2, and refresh it on server 1', async function () {
     this.timeout(60000)
 
-    await updateVideoChannel(servers[1].url, userServer2Token, 'channel1_server2', { displayName: 'channel updated' })
-    await updateMyUser({ url: servers[1].url, accessToken: userServer2Token, displayName: 'user updated' })
+    await servers[1].channels.update({
+      token: userServer2Token,
+      channelName: 'channel1_server2',
+      attributes: { displayName: 'channel updated' }
+    })
+    await servers[1].users.updateMe({ token: userServer2Token, displayName: 'user updated' })
 
     await waitJobs(servers)
     // Expire video channel
     await wait(10000)
 
     const search = 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2'
-    const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.have.lengthOf(1)
+    const body = await command.searchChannels({ search, token: servers[0].accessToken })
+    expect(body.total).to.equal(1)
+    expect(body.data).to.have.lengthOf(1)
 
-    const videoChannel: VideoChannel = res.body.data[0]
+    const videoChannel: VideoChannel = body.data[0]
     expect(videoChannel.displayName).to.equal('channel updated')
 
     // We don't return the owner account for now
@@ -190,8 +193,8 @@ describe('Test ActivityPub video channels search', function () {
   it('Should update and add a video on server 2, and update it on server 1 after a search', async function () {
     this.timeout(60000)
 
-    await updateVideo(servers[1].url, userServer2Token, videoServer2UUID, { name: 'video 1 updated' })
-    await uploadVideo(servers[1].url, userServer2Token, { name: 'video 2 server 2', channelId: channelIdServer2 })
+    await servers[1].videos.update({ token: userServer2Token, id: videoServer2UUID, attributes: { name: 'video 1 updated' } })
+    await servers[1].videos.upload({ token: userServer2Token, attributes: { name: 'video 2 server 2', channelId: channelIdServer2 } })
 
     await waitJobs(servers)
 
@@ -199,31 +202,31 @@ describe('Test ActivityPub video channels search', function () {
     await wait(10000)
 
     const search = 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2'
-    await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
+    await command.searchChannels({ search, token: servers[0].accessToken })
 
     await waitJobs(servers)
 
-    const videoChannelName = 'channel1_server2@localhost:' + servers[1].port
-    const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, videoChannelName, 0, 5, '-createdAt')
+    const handle = 'channel1_server2@localhost:' + servers[1].port
+    const { total, data } = await servers[0].videos.listByChannel({ handle, sort: '-createdAt' })
 
-    expect(res.body.total).to.equal(2)
-    expect(res.body.data[0].name).to.equal('video 2 server 2')
-    expect(res.body.data[1].name).to.equal('video 1 updated')
+    expect(total).to.equal(2)
+    expect(data[0].name).to.equal('video 2 server 2')
+    expect(data[1].name).to.equal('video 1 updated')
   })
 
   it('Should delete video channel of server 2, and delete it on server 1', async function () {
     this.timeout(60000)
 
-    await deleteVideoChannel(servers[1].url, userServer2Token, 'channel1_server2')
+    await servers[1].channels.delete({ token: userServer2Token, channelName: 'channel1_server2' })
 
     await waitJobs(servers)
     // Expire video
     await wait(10000)
 
     const search = 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2'
-    const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    const body = await command.searchChannels({ search, token: servers[0].accessToken })
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   after(async function () {
index 4c08e9548c04cf7b081baed4e8f2564d6a9863d8..33ca7be12af1af82cc8197bfb75d04e94daba50c 100644 (file)
 import 'mocha'
 import * as chai from 'chai'
 import {
-  addVideoInPlaylist,
   cleanupTests,
-  createVideoPlaylist,
-  deleteVideoPlaylist,
-  flushAndRunMultipleServers,
-  getVideoPlaylistsList,
-  searchVideoPlaylists,
-  ServerInfo,
+  createMultipleServers,
+  PeerTubeServer,
+  SearchCommand,
   setAccessTokensToServers,
   setDefaultVideoChannel,
-  uploadVideoAndGetId,
-  wait
-} from '../../../../shared/extra-utils'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { VideoPlaylist, VideoPlaylistPrivacy } from '../../../../shared/models/videos'
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { VideoPlaylistPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test ActivityPub playlists search', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let playlistServer1UUID: string
   let playlistServer2UUID: string
   let video2Server2: string
 
+  let command: SearchCommand
+
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
 
     {
-      const video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).uuid
-      const video2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).uuid
+      const video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).uuid
+      const video2 = (await servers[0].videos.quickUpload({ name: 'video 2' })).uuid
 
       const attributes = {
         displayName: 'playlist 1 on server 1',
         privacy: VideoPlaylistPrivacy.PUBLIC,
-        videoChannelId: servers[0].videoChannel.id
+        videoChannelId: servers[0].store.channel.id
       }
-      const res = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs: attributes })
-      playlistServer1UUID = res.body.videoPlaylist.uuid
+      const created = await servers[0].playlists.create({ attributes })
+      playlistServer1UUID = created.uuid
 
       for (const videoId of [ video1, video2 ]) {
-        await addVideoInPlaylist({
-          url: servers[0].url,
-          token: servers[0].accessToken,
-          playlistId: playlistServer1UUID,
-          elementAttrs: { videoId }
-        })
+        await servers[0].playlists.addElement({ playlistId: playlistServer1UUID, attributes: { videoId } })
       }
     }
 
     {
-      const videoId = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 1' })).uuid
-      video2Server2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 2' })).uuid
+      const videoId = (await servers[1].videos.quickUpload({ name: 'video 1' })).uuid
+      video2Server2 = (await servers[1].videos.quickUpload({ name: 'video 2' })).uuid
 
       const attributes = {
         displayName: 'playlist 1 on server 2',
         privacy: VideoPlaylistPrivacy.PUBLIC,
-        videoChannelId: servers[1].videoChannel.id
+        videoChannelId: servers[1].store.channel.id
       }
-      const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs: attributes })
-      playlistServer2UUID = res.body.videoPlaylist.uuid
-
-      await addVideoInPlaylist({
-        url: servers[1].url,
-        token: servers[1].accessToken,
-        playlistId: playlistServer2UUID,
-        elementAttrs: { videoId }
-      })
+      const created = await servers[1].playlists.create({ attributes })
+      playlistServer2UUID = created.uuid
+
+      await servers[1].playlists.addElement({ playlistId: playlistServer2UUID, attributes: { videoId } })
     }
 
     await waitJobs(servers)
+
+    command = servers[0].search
   })
 
   it('Should not find a remote playlist', async function () {
     {
       const search = 'http://localhost:' + servers[1].port + '/video-playlists/43'
-      const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken)
+      const body = await command.searchPlaylists({ search, token: servers[0].accessToken })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     }
 
     {
       // Without token
       const search = 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID
-      const res = await searchVideoPlaylists(servers[0].url, search)
+      const body = await command.searchPlaylists({ search })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     }
   })
 
   it('Should search a local playlist', async function () {
     const search = 'http://localhost:' + servers[0].port + '/video-playlists/' + playlistServer1UUID
-    const res = await searchVideoPlaylists(servers[0].url, search)
+    const body = await command.searchPlaylists({ search })
 
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data).to.have.lengthOf(1)
-    expect(res.body.data[0].displayName).to.equal('playlist 1 on server 1')
-    expect(res.body.data[0].videosLength).to.equal(2)
+    expect(body.total).to.equal(1)
+    expect(body.data).to.be.an('array')
+    expect(body.data).to.have.lengthOf(1)
+    expect(body.data[0].displayName).to.equal('playlist 1 on server 1')
+    expect(body.data[0].videosLength).to.equal(2)
   })
 
   it('Should search a local playlist with an alternative URL', async function () {
@@ -120,13 +109,13 @@ describe('Test ActivityPub playlists search', function () {
 
     for (const search of searches) {
       for (const token of [ undefined, servers[0].accessToken ]) {
-        const res = await searchVideoPlaylists(servers[0].url, search, token)
+        const body = await command.searchPlaylists({ search, token })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.be.an('array')
-        expect(res.body.data).to.have.lengthOf(1)
-        expect(res.body.data[0].displayName).to.equal('playlist 1 on server 1')
-        expect(res.body.data[0].videosLength).to.equal(2)
+        expect(body.total).to.equal(1)
+        expect(body.data).to.be.an('array')
+        expect(body.data).to.have.lengthOf(1)
+        expect(body.data[0].displayName).to.equal('playlist 1 on server 1')
+        expect(body.data[0].videosLength).to.equal(2)
       }
     }
   })
@@ -139,32 +128,27 @@ describe('Test ActivityPub playlists search', function () {
     ]
 
     for (const search of searches) {
-      const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken)
+      const body = await command.searchPlaylists({ search, token: servers[0].accessToken })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].displayName).to.equal('playlist 1 on server 2')
-      expect(res.body.data[0].videosLength).to.equal(1)
+      expect(body.total).to.equal(1)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].displayName).to.equal('playlist 1 on server 2')
+      expect(body.data[0].videosLength).to.equal(1)
     }
   })
 
   it('Should not list this remote playlist', async function () {
-    const res = await getVideoPlaylistsList(servers[0].url, 0, 10)
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.have.lengthOf(1)
-    expect(res.body.data[0].displayName).to.equal('playlist 1 on server 1')
+    const body = await servers[0].playlists.list({ start: 0, count: 10 })
+    expect(body.total).to.equal(1)
+    expect(body.data).to.have.lengthOf(1)
+    expect(body.data[0].displayName).to.equal('playlist 1 on server 1')
   })
 
   it('Should update the playlist of server 2, and refresh it on server 1', async function () {
     this.timeout(60000)
 
-    await addVideoInPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistServer2UUID,
-      elementAttrs: { videoId: video2Server2 }
-    })
+    await servers[1].playlists.addElement({ playlistId: playlistServer2UUID, attributes: { videoId: video2Server2 } })
 
     await waitJobs(servers)
     // Expire playlist
@@ -172,23 +156,23 @@ describe('Test ActivityPub playlists search', function () {
 
     // Will run refresh async
     const search = 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID
-    await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken)
+    await command.searchPlaylists({ search, token: servers[0].accessToken })
 
     // Wait refresh
     await wait(5000)
 
-    const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken)
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.have.lengthOf(1)
+    const body = await command.searchPlaylists({ search, token: servers[0].accessToken })
+    expect(body.total).to.equal(1)
+    expect(body.data).to.have.lengthOf(1)
 
-    const playlist: VideoPlaylist = res.body.data[0]
+    const playlist = body.data[0]
     expect(playlist.videosLength).to.equal(2)
   })
 
   it('Should delete playlist of server 2, and delete it on server 1', async function () {
     this.timeout(60000)
 
-    await deleteVideoPlaylist(servers[1].url, servers[1].accessToken, playlistServer2UUID)
+    await servers[1].playlists.delete({ playlistId: playlistServer2UUID })
 
     await waitJobs(servers)
     // Expiration
@@ -196,14 +180,14 @@ describe('Test ActivityPub playlists search', function () {
 
     // Will run refresh async
     const search = 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID
-    await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken)
+    await command.searchPlaylists({ search, token: servers[0].accessToken })
 
     // Wait refresh
     await wait(5000)
 
-    const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken)
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    const body = await command.searchPlaylists({ search, token: servers[0].accessToken })
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   after(async function () {
index e9b4978da95b6f2a8f67aeb6ad1f80fd1d62dbd9..b3cfcacca99e796faab0b0c746b0a3a6857a44f2 100644 (file)
@@ -1,92 +1,90 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import {
-  addVideoChannel,
   cleanupTests,
-  flushAndRunMultipleServers,
-  getVideosList,
-  removeVideo,
-  searchVideo,
-  searchVideoWithToken,
-  ServerInfo,
+  createMultipleServers,
+  PeerTubeServer,
+  SearchCommand,
   setAccessTokensToServers,
-  updateVideo,
-  uploadVideo,
-  wait
-} from '../../../../shared/extra-utils'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { Video, VideoPrivacy } from '../../../../shared/models/videos'
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test ActivityPub videos search', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let videoServer1UUID: string
   let videoServer2UUID: string
 
+  let command: SearchCommand
+
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
 
     {
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 1 on server 1' })
-      videoServer1UUID = res.body.video.uuid
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1 on server 1' } })
+      videoServer1UUID = uuid
     }
 
     {
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 on server 2' })
-      videoServer2UUID = res.body.video.uuid
+      const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 1 on server 2' } })
+      videoServer2UUID = uuid
     }
 
     await waitJobs(servers)
+
+    command = servers[0].search
   })
 
   it('Should not find a remote video', async function () {
     {
       const search = 'http://localhost:' + servers[1].port + '/videos/watch/43'
-      const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
+      const body = await command.searchVideos({ search, token: servers[0].accessToken })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     }
 
     {
       // Without token
       const search = 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID
-      const res = await searchVideo(servers[0].url, search)
+      const body = await command.searchVideos({ search })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     }
   })
 
   it('Should search a local video', async function () {
     const search = 'http://localhost:' + servers[0].port + '/videos/watch/' + videoServer1UUID
-    const res = await searchVideo(servers[0].url, search)
+    const body = await command.searchVideos({ search })
 
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data).to.have.lengthOf(1)
-    expect(res.body.data[0].name).to.equal('video 1 on server 1')
+    expect(body.total).to.equal(1)
+    expect(body.data).to.be.an('array')
+    expect(body.data).to.have.lengthOf(1)
+    expect(body.data[0].name).to.equal('video 1 on server 1')
   })
 
   it('Should search a local video with an alternative URL', async function () {
     const search = 'http://localhost:' + servers[0].port + '/w/' + videoServer1UUID
-    const res1 = await searchVideo(servers[0].url, search)
-    const res2 = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
-
-    for (const res of [ res1, res2 ]) {
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].name).to.equal('video 1 on server 1')
+    const body1 = await command.searchVideos({ search })
+    const body2 = await command.searchVideos({ search, token: servers[0].accessToken })
+
+    for (const body of [ body1, body2 ]) {
+      expect(body.total).to.equal(1)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].name).to.equal('video 1 on server 1')
     }
   })
 
@@ -97,20 +95,20 @@ describe('Test ActivityPub videos search', function () {
     ]
 
     for (const search of searches) {
-      const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
+      const body = await command.searchVideos({ search, token: servers[0].accessToken })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].name).to.equal('video 1 on server 2')
+      expect(body.total).to.equal(1)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].name).to.equal('video 1 on server 2')
     }
   })
 
   it('Should not list this remote video', async function () {
-    const res = await getVideosList(servers[0].url)
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.have.lengthOf(1)
-    expect(res.body.data[0].name).to.equal('video 1 on server 1')
+    const { total, data } = await servers[0].videos.list()
+    expect(total).to.equal(1)
+    expect(data).to.have.lengthOf(1)
+    expect(data[0].name).to.equal('video 1 on server 1')
   })
 
   it('Should update video of server 2, and refresh it on server 1', async function () {
@@ -120,8 +118,8 @@ describe('Test ActivityPub videos search', function () {
       name: 'super_channel',
       displayName: 'super channel'
     }
-    const resChannel = await addVideoChannel(servers[1].url, servers[1].accessToken, channelAttributes)
-    const videoChannelId = resChannel.body.videoChannel.id
+    const created = await servers[1].channels.create({ attributes: channelAttributes })
+    const videoChannelId = created.id
 
     const attributes = {
       name: 'updated',
@@ -129,7 +127,7 @@ describe('Test ActivityPub videos search', function () {
       privacy: VideoPrivacy.UNLISTED,
       channelId: videoChannelId
     }
-    await updateVideo(servers[1].url, servers[1].accessToken, videoServer2UUID, attributes)
+    await servers[1].videos.update({ id: videoServer2UUID, attributes })
 
     await waitJobs(servers)
     // Expire video
@@ -137,16 +135,16 @@ describe('Test ActivityPub videos search', function () {
 
     // Will run refresh async
     const search = 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID
-    await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
+    await command.searchVideos({ search, token: servers[0].accessToken })
 
     // Wait refresh
     await wait(5000)
 
-    const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.have.lengthOf(1)
+    const body = await command.searchVideos({ search, token: servers[0].accessToken })
+    expect(body.total).to.equal(1)
+    expect(body.data).to.have.lengthOf(1)
 
-    const video: Video = res.body.data[0]
+    const video = body.data[0]
     expect(video.name).to.equal('updated')
     expect(video.channel.name).to.equal('super_channel')
     expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
@@ -155,7 +153,7 @@ describe('Test ActivityPub videos search', function () {
   it('Should delete video of server 2, and delete it on server 1', async function () {
     this.timeout(120000)
 
-    await removeVideo(servers[1].url, servers[1].accessToken, videoServer2UUID)
+    await servers[1].videos.remove({ id: videoServer2UUID })
 
     await waitJobs(servers)
     // Expire video
@@ -163,14 +161,14 @@ describe('Test ActivityPub videos search', function () {
 
     // Will run refresh async
     const search = 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID
-    await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
+    await command.searchVideos({ search, token: servers[0].accessToken })
 
     // Wait refresh
     await wait(5000)
 
-    const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    const body = await command.searchVideos({ search, token: servers[0].accessToken })
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   after(async function () {
index daca2aebe5154a8eced2f769f603d537b437c5ea..8a01aff90a675c44117b974a406af882ec9179f8 100644 (file)
@@ -2,44 +2,65 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { searchVideoChannel, advancedVideoChannelSearch } from '@shared/extra-utils/search/video-channels'
 import {
-  addVideoChannel,
   cleanupTests,
-  createUser,
-  flushAndRunServer,
-  ServerInfo,
+  createSingleServer,
+  doubleFollow,
+  PeerTubeServer,
+  SearchCommand,
   setAccessTokensToServers
-} from '../../../../shared/extra-utils'
+} from '@shared/extra-utils'
 import { VideoChannel } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test channels search', function () {
-  let server: ServerInfo = null
+  let server: PeerTubeServer
+  let remoteServer: PeerTubeServer
+  let command: SearchCommand
 
   before(async function () {
-    this.timeout(30000)
+    this.timeout(120000)
 
-    server = await flushAndRunServer(1)
+    const servers = await Promise.all([
+      createSingleServer(1),
+      createSingleServer(2, { transcoding: { enabled: false } })
+    ])
+    server = servers[0]
+    remoteServer = servers[1]
 
-    await setAccessTokensToServers([ server ])
+    await setAccessTokensToServers([ server, remoteServer ])
 
     {
-      await createUser({ url: server.url, accessToken: server.accessToken, username: 'user1', password: 'password' })
+      await server.users.create({ username: 'user1' })
       const channel = {
         name: 'squall_channel',
         displayName: 'Squall channel'
       }
-      await addVideoChannel(server.url, server.accessToken, channel)
+      await server.channels.create({ attributes: channel })
     }
+
+    {
+      await remoteServer.users.create({ username: 'user1' })
+      const channel = {
+        name: 'zell_channel',
+        displayName: 'Zell channel'
+      }
+      const { id } = await remoteServer.channels.create({ attributes: channel })
+
+      await remoteServer.videos.upload({ attributes: { channelId: id } })
+    }
+
+    await doubleFollow(server, remoteServer)
+
+    command = server.search
   })
 
   it('Should make a simple search and not have results', async function () {
-    const res = await searchVideoChannel(server.url, 'abc')
+    const body = await command.searchChannels({ search: 'abc' })
 
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   it('Should make a search and have results', async function () {
@@ -49,11 +70,11 @@ describe('Test channels search', function () {
         start: 0,
         count: 1
       }
-      const res = await advancedVideoChannelSearch(server.url, search)
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
+      const body = await command.advancedChannelSearch({ search })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
 
-      const channel: VideoChannel = res.body.data[0]
+      const channel: VideoChannel = body.data[0]
       expect(channel.name).to.equal('squall_channel')
       expect(channel.displayName).to.equal('Squall channel')
     }
@@ -65,15 +86,64 @@ describe('Test channels search', function () {
         count: 1
       }
 
-      const res = await advancedVideoChannelSearch(server.url, search)
+      const body = await command.advancedChannelSearch({ search })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(0)
+    }
+  })
+
+  it('Should filter by host', async function () {
+    {
+      const search = { search: 'channel', host: remoteServer.host }
 
-      expect(res.body.total).to.equal(1)
+      const body = await command.advancedChannelSearch({ search })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].displayName).to.equal('Zell channel')
+    }
+
+    {
+      const search = { search: 'Sq', host: server.host }
 
-      expect(res.body.data).to.have.lengthOf(0)
+      const body = await command.advancedChannelSearch({ search })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].displayName).to.equal('Squall channel')
+    }
+
+    {
+      const search = { search: 'Squall', host: 'example.com' }
+
+      const body = await command.advancedChannelSearch({ search })
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
+    }
+  })
+
+  it('Should filter by names', async function () {
+    {
+      const body = await command.advancedChannelSearch({ search: { handles: [ 'squall_channel', 'zell_channel' ] } })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].displayName).to.equal('Squall channel')
+    }
+
+    {
+      const body = await command.advancedChannelSearch({ search: { handles: [ 'chocobozzz_channel' ] } })
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
+    }
+
+    {
+      const body = await command.advancedChannelSearch({ search: { handles: [ 'squall_channel', 'zell_channel@' + remoteServer.host ] } })
+      expect(body.total).to.equal(2)
+      expect(body.data).to.have.lengthOf(2)
+      expect(body.data[0].displayName).to.equal('Squall channel')
+      expect(body.data[1].displayName).to.equal('Zell channel')
     }
   })
 
   after(async function () {
-    await cleanupTests([ server ])
+    await cleanupTests([ server, remoteServer ])
   })
 })
index 00f79232ab46777ecb75e5da4afc2ac089e08a03..4c8b1f6084ccd6c81afa51d135c6cf1c236ea86f 100644 (file)
@@ -2,36 +2,34 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { advancedVideoChannelSearch, searchVideoChannel } from '@shared/extra-utils/search/video-channels'
-import { Video, VideoChannel, VideoPlaylist, VideoPlaylistPrivacy, VideoPlaylistType, VideosSearchQuery } from '@shared/models'
+import { cleanupTests, createSingleServer, PeerTubeServer, SearchCommand, setAccessTokensToServers } from '@shared/extra-utils'
 import {
-  advancedVideoPlaylistSearch,
-  advancedVideosSearch,
-  cleanupTests,
-  flushAndRunServer,
-  immutableAssign,
-  searchVideo,
-  searchVideoPlaylists,
-  ServerInfo,
-  setAccessTokensToServers,
-  updateCustomSubConfig,
-  uploadVideo
-} from '../../../../shared/extra-utils'
+  BooleanBothQuery,
+  VideoChannelsSearchQuery,
+  VideoPlaylistPrivacy,
+  VideoPlaylistsSearchQuery,
+  VideoPlaylistType,
+  VideosSearchQuery
+} from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test videos search', function () {
-  let server: ServerInfo = null
   const localVideoName = 'local video' + new Date().toISOString()
 
+  let server: PeerTubeServer = null
+  let command: SearchCommand
+
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
-    await uploadVideo(server.url, server.accessToken, { name: localVideoName })
+    await server.videos.upload({ attributes: { name: localVideoName } })
+
+    command = server.search
   })
 
   describe('Default search', async function () {
@@ -39,163 +37,213 @@ describe('Test videos search', function () {
     it('Should make a local videos search by default', async function () {
       this.timeout(10000)
 
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        search: {
-          searchIndex: {
-            enabled: true,
-            isDefaultSearch: false,
-            disableLocalSearch: false
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          search: {
+            searchIndex: {
+              enabled: true,
+              isDefaultSearch: false,
+              disableLocalSearch: false
+            }
           }
         }
       })
 
-      const res = await searchVideo(server.url, 'local video')
+      const body = await command.searchVideos({ search: 'local video' })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data[0].name).to.equal(localVideoName)
+      expect(body.total).to.equal(1)
+      expect(body.data[0].name).to.equal(localVideoName)
     })
 
     it('Should make a local channels search by default', async function () {
-      const res = await searchVideoChannel(server.url, 'root')
+      const body = await command.searchChannels({ search: 'root' })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data[0].name).to.equal('root_channel')
-      expect(res.body.data[0].host).to.equal('localhost:' + server.port)
+      expect(body.total).to.equal(1)
+      expect(body.data[0].name).to.equal('root_channel')
+      expect(body.data[0].host).to.equal('localhost:' + server.port)
     })
 
     it('Should make an index videos search by default', async function () {
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        search: {
-          searchIndex: {
-            enabled: true,
-            isDefaultSearch: true,
-            disableLocalSearch: false
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          search: {
+            searchIndex: {
+              enabled: true,
+              isDefaultSearch: true,
+              disableLocalSearch: false
+            }
           }
         }
       })
 
-      const res = await searchVideo(server.url, 'local video')
-      expect(res.body.total).to.be.greaterThan(2)
+      const body = await command.searchVideos({ search: 'local video' })
+      expect(body.total).to.be.greaterThan(2)
     })
 
     it('Should make an index channels search by default', async function () {
-      const res = await searchVideoChannel(server.url, 'root')
-      expect(res.body.total).to.be.greaterThan(2)
+      const body = await command.searchChannels({ search: 'root' })
+      expect(body.total).to.be.greaterThan(2)
     })
 
     it('Should make an index videos search if local search is disabled', async function () {
-      await updateCustomSubConfig(server.url, server.accessToken, {
-        search: {
-          searchIndex: {
-            enabled: true,
-            isDefaultSearch: false,
-            disableLocalSearch: true
+      await server.config.updateCustomSubConfig({
+        newConfig: {
+          search: {
+            searchIndex: {
+              enabled: true,
+              isDefaultSearch: false,
+              disableLocalSearch: true
+            }
           }
         }
       })
 
-      const res = await searchVideo(server.url, 'local video')
-      expect(res.body.total).to.be.greaterThan(2)
+      const body = await command.searchVideos({ search: 'local video' })
+      expect(body.total).to.be.greaterThan(2)
     })
 
     it('Should make an index channels search if local search is disabled', async function () {
-      const res = await searchVideoChannel(server.url, 'root')
-      expect(res.body.total).to.be.greaterThan(2)
+      const body = await command.searchChannels({ search: 'root' })
+      expect(body.total).to.be.greaterThan(2)
     })
   })
 
   describe('Videos search', async function () {
 
+    async function check (search: VideosSearchQuery, exists = true) {
+      const body = await command.advancedVideoSearch({ search })
+
+      if (exists === false) {
+        expect(body.total).to.equal(0)
+        expect(body.data).to.have.lengthOf(0)
+        return
+      }
+
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
+
+      const video = body.data[0]
+
+      expect(video.name).to.equal('What is PeerTube?')
+      expect(video.category.label).to.equal('Science & Technology')
+      expect(video.licence.label).to.equal('Attribution - Share Alike')
+      expect(video.privacy.label).to.equal('Public')
+      expect(video.duration).to.equal(113)
+      expect(video.thumbnailUrl.startsWith('https://framatube.org/static/thumbnails')).to.be.true
+
+      expect(video.account.host).to.equal('framatube.org')
+      expect(video.account.name).to.equal('framasoft')
+      expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft')
+      expect(video.account.avatar).to.exist
+
+      expect(video.channel.host).to.equal('framatube.org')
+      expect(video.channel.name).to.equal('bf54d359-cfad-4935-9d45-9d6be93f63e8')
+      expect(video.channel.url).to.equal('https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8')
+      expect(video.channel.avatar).to.exist
+    }
+
+    const baseSearch: VideosSearchQuery = {
+      search: 'what is peertube',
+      start: 0,
+      count: 2,
+      categoryOneOf: [ 15 ],
+      licenceOneOf: [ 2 ],
+      tagsAllOf: [ 'framasoft', 'peertube' ],
+      startDate: '2018-10-01T10:50:46.396Z',
+      endDate: '2018-10-01T10:55:46.396Z'
+    }
+
     it('Should make a simple search and not have results', async function () {
-      const res = await searchVideo(server.url, 'djidane'.repeat(50))
+      const body = await command.searchVideos({ search: 'djidane'.repeat(50) })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
     })
 
     it('Should make a simple search and have results', async function () {
-      const res = await searchVideo(server.url, 'What is PeerTube')
+      const body = await command.searchVideos({ search: 'What is PeerTube' })
 
-      expect(res.body.total).to.be.greaterThan(1)
+      expect(body.total).to.be.greaterThan(1)
     })
 
-    it('Should make a complex search', async function () {
-
-      async function check (search: VideosSearchQuery, exists = true) {
-        const res = await advancedVideosSearch(server.url, search)
-
-        if (exists === false) {
-          expect(res.body.total).to.equal(0)
-          expect(res.body.data).to.have.lengthOf(0)
-          return
-        }
-
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
-
-        const video: Video = res.body.data[0]
-
-        expect(video.name).to.equal('What is PeerTube?')
-        expect(video.category.label).to.equal('Science & Technology')
-        expect(video.licence.label).to.equal('Attribution - Share Alike')
-        expect(video.privacy.label).to.equal('Public')
-        expect(video.duration).to.equal(113)
-        expect(video.thumbnailUrl.startsWith('https://framatube.org/static/thumbnails')).to.be.true
+    it('Should make a simple search', async function () {
+      await check(baseSearch)
+    })
 
-        expect(video.account.host).to.equal('framatube.org')
-        expect(video.account.name).to.equal('framasoft')
-        expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft')
-        expect(video.account.avatar).to.exist
+    it('Should search by start date', async function () {
+      const search = { ...baseSearch, startDate: '2018-10-01T10:54:46.396Z' }
+      await check(search, false)
+    })
 
-        expect(video.channel.host).to.equal('framatube.org')
-        expect(video.channel.name).to.equal('bf54d359-cfad-4935-9d45-9d6be93f63e8')
-        expect(video.channel.url).to.equal('https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8')
-        expect(video.channel.avatar).to.exist
-      }
+    it('Should search by tags', async function () {
+      const search = { ...baseSearch, tagsAllOf: [ 'toto', 'framasoft' ] }
+      await check(search, false)
+    })
 
-      const baseSearch: VideosSearchQuery = {
-        search: 'what is peertube',
-        start: 0,
-        count: 2,
-        categoryOneOf: [ 15 ],
-        licenceOneOf: [ 2 ],
-        tagsAllOf: [ 'framasoft', 'peertube' ],
-        startDate: '2018-10-01T10:50:46.396Z',
-        endDate: '2018-10-01T10:55:46.396Z'
-      }
+    it('Should search by duration', async function () {
+      const search = { ...baseSearch, durationMin: 2000 }
+      await check(search, false)
+    })
 
+    it('Should search by nsfw attribute', async function () {
       {
-        await check(baseSearch)
+        const search = { ...baseSearch, nsfw: 'true' as BooleanBothQuery }
+        await check(search, false)
       }
 
       {
-        const search = immutableAssign(baseSearch, { startDate: '2018-10-01T10:54:46.396Z' })
-        await check(search, false)
+        const search = { ...baseSearch, nsfw: 'false' as BooleanBothQuery }
+        await check(search, true)
       }
 
       {
-        const search = immutableAssign(baseSearch, { tagsAllOf: [ 'toto', 'framasoft' ] })
-        await check(search, false)
+        const search = { ...baseSearch, nsfw: 'both' as BooleanBothQuery }
+        await check(search, true)
       }
+    })
 
+    it('Should search by host', async function () {
       {
-        const search = immutableAssign(baseSearch, { durationMin: 2000 })
+        const search = { ...baseSearch, host: 'example.com' }
         await check(search, false)
       }
 
       {
-        const search = immutableAssign(baseSearch, { nsfw: 'true' })
-        await check(search, false)
+        const search = { ...baseSearch, host: 'framatube.org' }
+        await check(search, true)
       }
+    })
+
+    it('Should search by uuids', async function () {
+      const goodUUID = '9c9de5e8-0a1e-484a-b099-e80766180a6d'
+      const goodShortUUID = 'kkGMgK9ZtnKfYAgnEtQxbv'
+      const badUUID = 'c29c5b77-4a04-493d-96a9-2e9267e308f0'
+      const badShortUUID = 'rP5RgUeX9XwTSrspCdkDej'
 
       {
-        const search = immutableAssign(baseSearch, { nsfw: 'false' })
-        await check(search, true)
+        const uuidsMatrix = [
+          [ goodUUID ],
+          [ goodUUID, badShortUUID ],
+          [ badShortUUID, goodShortUUID ],
+          [ goodUUID, goodShortUUID ]
+        ]
+
+        for (const uuids of uuidsMatrix) {
+          const search = { ...baseSearch, uuids }
+          await check(search, true)
+        }
       }
 
       {
-        const search = immutableAssign(baseSearch, { nsfw: 'both' })
-        await check(search, true)
+        const uuidsMatrix = [
+          [ badUUID ],
+          [ badShortUUID ]
+        ]
+
+        for (const uuids of uuidsMatrix) {
+          const search = { ...baseSearch, uuids }
+          await check(search, false)
+        }
       }
     })
 
@@ -206,37 +254,44 @@ describe('Test videos search', function () {
         count: 5
       }
 
-      const res = await advancedVideosSearch(server.url, search)
+      const body = await command.advancedVideoSearch({ search })
 
-      expect(res.body.total).to.be.greaterThan(5)
-      expect(res.body.data).to.have.lengthOf(5)
+      expect(body.total).to.be.greaterThan(5)
+      expect(body.data).to.have.lengthOf(5)
     })
 
     it('Should use the nsfw instance policy as default', async function () {
       let nsfwUUID: string
 
       {
-        await updateCustomSubConfig(server.url, server.accessToken, { instance: { defaultNSFWPolicy: 'display' } })
+        await server.config.updateCustomSubConfig({
+          newConfig: {
+            instance: { defaultNSFWPolicy: 'display' }
+          }
+        })
 
-        const res = await searchVideo(server.url, 'NSFW search index', '-match')
-        const video = res.body.data[0] as Video
+        const body = await command.searchVideos({ search: 'NSFW search index', sort: '-match' })
+        expect(body.data).to.have.length.greaterThan(0)
 
-        expect(res.body.data).to.have.length.greaterThan(0)
+        const video = body.data[0]
         expect(video.nsfw).to.be.true
 
         nsfwUUID = video.uuid
       }
 
       {
-        await updateCustomSubConfig(server.url, server.accessToken, { instance: { defaultNSFWPolicy: 'do_not_list' } })
+        await server.config.updateCustomSubConfig({
+          newConfig: {
+            instance: { defaultNSFWPolicy: 'do_not_list' }
+          }
+        })
 
-        const res = await searchVideo(server.url, 'NSFW search index', '-match')
+        const body = await command.searchVideos({ search: 'NSFW search index', sort: '-match' })
 
         try {
-          expect(res.body.data).to.have.lengthOf(0)
-        } catch (err) {
-          //
-          const video = res.body.data[0] as Video
+          expect(body.data).to.have.lengthOf(0)
+        } catch {
+          const video = body.data[0]
 
           expect(video.uuid).not.equal(nsfwUUID)
         }
@@ -246,20 +301,19 @@ describe('Test videos search', function () {
 
   describe('Channels search', async function () {
 
-    it('Should make a simple search and not have results', async function () {
-      const res = await searchVideoChannel(server.url, 'a'.repeat(500))
+    async function check (search: VideoChannelsSearchQuery, exists = true) {
+      const body = await command.advancedChannelSearch({ search })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.have.lengthOf(0)
-    })
-
-    it('Should make a search and have results', async function () {
-      const res = await advancedVideoChannelSearch(server.url, { search: 'Framasoft', sort: 'createdAt' })
+      if (exists === false) {
+        expect(body.total).to.equal(0)
+        expect(body.data).to.have.lengthOf(0)
+        return
+      }
 
-      expect(res.body.total).to.be.greaterThan(0)
-      expect(res.body.data).to.have.length.greaterThan(0)
+      expect(body.total).to.be.greaterThan(0)
+      expect(body.data).to.have.length.greaterThan(0)
 
-      const videoChannel: VideoChannel = res.body.data[0]
+      const videoChannel = body.data[0]
       expect(videoChannel.url).to.equal('https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8')
       expect(videoChannel.host).to.equal('framatube.org')
       expect(videoChannel.avatar).to.exist
@@ -269,32 +323,53 @@ describe('Test videos search', function () {
       expect(videoChannel.ownerAccount.name).to.equal('framasoft')
       expect(videoChannel.ownerAccount.host).to.equal('framatube.org')
       expect(videoChannel.ownerAccount.avatar).to.exist
+    }
+
+    it('Should make a simple search and not have results', async function () {
+      const body = await command.searchChannels({ search: 'a'.repeat(500) })
+
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
+    })
+
+    it('Should make a search and have results', async function () {
+      await check({ search: 'Framasoft', sort: 'createdAt' }, true)
+    })
+
+    it('Should make host search and have appropriate results', async function () {
+      await check({ search: 'Framasoft', host: 'example.com' }, false)
+      await check({ search: 'Framasoft', host: 'framatube.org' }, true)
+    })
+
+    it('Should make handles search and have appropriate results', async function () {
+      await check({ handles: [ 'bf54d359-cfad-4935-9d45-9d6be93f63e8@framatube.org' ] }, true)
+      await check({ handles: [ 'jeanine', 'bf54d359-cfad-4935-9d45-9d6be93f63e8@framatube.org' ] }, true)
+      await check({ handles: [ 'jeanine', 'chocobozzz_channel2@peertube2.cpy.re' ] }, false)
     })
 
     it('Should have a correct pagination', async function () {
-      const res = await advancedVideoChannelSearch(server.url, { search: 'root', start: 0, count: 2 })
+      const body = await command.advancedChannelSearch({ search: { search: 'root', start: 0, count: 2 } })
 
-      expect(res.body.total).to.be.greaterThan(2)
-      expect(res.body.data).to.have.lengthOf(2)
+      expect(body.total).to.be.greaterThan(2)
+      expect(body.data).to.have.lengthOf(2)
     })
   })
 
   describe('Playlists search', async function () {
 
-    it('Should make a simple search and not have results', async function () {
-      const res = await searchVideoPlaylists(server.url, 'a'.repeat(500))
+    async function check (search: VideoPlaylistsSearchQuery, exists = true) {
+      const body = await command.advancedPlaylistSearch({ search })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.have.lengthOf(0)
-    })
-
-    it('Should make a search and have results', async function () {
-      const res = await advancedVideoPlaylistSearch(server.url, { search: 'E2E playlist', sort: '-match' })
+      if (exists === false) {
+        expect(body.total).to.equal(0)
+        expect(body.data).to.have.lengthOf(0)
+        return
+      }
 
-      expect(res.body.total).to.be.greaterThan(0)
-      expect(res.body.data).to.have.length.greaterThan(0)
+      expect(body.total).to.be.greaterThan(0)
+      expect(body.data).to.have.length.greaterThan(0)
 
-      const videoPlaylist: VideoPlaylist = res.body.data[0]
+      const videoPlaylist = body.data[0]
 
       expect(videoPlaylist.url).to.equal('https://peertube2.cpy.re/videos/watch/playlist/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a')
       expect(videoPlaylist.thumbnailUrl).to.exist
@@ -319,13 +394,62 @@ describe('Test videos search', function () {
       expect(videoPlaylist.videoChannel.name).to.equal('chocobozzz_channel')
       expect(videoPlaylist.videoChannel.host).to.equal('peertube2.cpy.re')
       expect(videoPlaylist.videoChannel.avatar).to.exist
+    }
+
+    it('Should make a simple search and not have results', async function () {
+      const body = await command.searchPlaylists({ search: 'a'.repeat(500) })
+
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
+    })
+
+    it('Should make a search and have results', async function () {
+      await check({ search: 'E2E playlist', sort: '-match' }, true)
+    })
+
+    it('Should make host search and have appropriate results', async function () {
+      await check({ search: 'E2E playlist', host: 'example.com' }, false)
+      await check({ search: 'E2E playlist', host: 'peertube2.cpy.re', sort: '-match' }, true)
+    })
+
+    it('Should make a search by uuids and have appropriate results', async function () {
+      const goodUUID = '73804a40-da9a-40c2-b1eb-2c6d9eec8f0a'
+      const goodShortUUID = 'fgei1ws1oa6FCaJ2qZPG29'
+      const badUUID = 'c29c5b77-4a04-493d-96a9-2e9267e308f0'
+      const badShortUUID = 'rP5RgUeX9XwTSrspCdkDej'
+
+      {
+        const uuidsMatrix = [
+          [ goodUUID ],
+          [ goodUUID, badShortUUID ],
+          [ badShortUUID, goodShortUUID ],
+          [ goodUUID, goodShortUUID ]
+        ]
+
+        for (const uuids of uuidsMatrix) {
+          const search = { search: 'E2E playlist', sort: '-match', uuids }
+          await check(search, true)
+        }
+      }
+
+      {
+        const uuidsMatrix = [
+          [ badUUID ],
+          [ badShortUUID ]
+        ]
+
+        for (const uuids of uuidsMatrix) {
+          const search = { search: 'E2E playlist', sort: '-match', uuids }
+          await check(search, false)
+        }
+      }
     })
 
     it('Should have a correct pagination', async function () {
-      const res = await advancedVideoChannelSearch(server.url, { search: 'root', start: 0, count: 2 })
+      const body = await command.advancedChannelSearch({ search: { search: 'root', start: 0, count: 2 } })
 
-      expect(res.body.total).to.be.greaterThan(2)
-      expect(res.body.data).to.have.lengthOf(2)
+      expect(body.total).to.be.greaterThan(2)
+      expect(body.data).to.have.lengthOf(2)
     })
   })
 
index ab17d55e9719d387dd9e316725b2bfdb2e90f4e7..15aac029a3bc6749b224108216bc4abfbc1a50bf 100644 (file)
@@ -2,82 +2,86 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { VideoPlaylist, VideoPlaylistPrivacy } from '@shared/models'
 import {
-  addVideoInPlaylist,
-  advancedVideoPlaylistSearch,
   cleanupTests,
-  createVideoPlaylist,
-  flushAndRunServer,
-  searchVideoPlaylists,
-  ServerInfo,
+  createSingleServer,
+  doubleFollow,
+  PeerTubeServer,
+  SearchCommand,
   setAccessTokensToServers,
-  setDefaultVideoChannel,
-  uploadVideoAndGetId
-} from '../../../../shared/extra-utils'
+  setDefaultVideoChannel
+} from '@shared/extra-utils'
+import { VideoPlaylistPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test playlists search', function () {
-  let server: ServerInfo = null
+  let server: PeerTubeServer
+  let remoteServer: PeerTubeServer
+  let command: SearchCommand
+  let playlistUUID: string
+  let playlistShortUUID: string
 
   before(async function () {
-    this.timeout(30000)
+    this.timeout(120000)
 
-    server = await flushAndRunServer(1)
+    const servers = await Promise.all([
+      createSingleServer(1),
+      createSingleServer(2, { transcoding: { enabled: false } })
+    ])
+    server = servers[0]
+    remoteServer = servers[1]
 
-    await setAccessTokensToServers([ server ])
-    await setDefaultVideoChannel([ server ])
-
-    const videoId = (await uploadVideoAndGetId({ server: server, videoName: 'video' })).uuid
+    await setAccessTokensToServers([ remoteServer, server ])
+    await setDefaultVideoChannel([ remoteServer, server ])
 
     {
+      const videoId = (await server.videos.upload()).uuid
+
       const attributes = {
         displayName: 'Dr. Kenzo Tenma hospital videos',
         privacy: VideoPlaylistPrivacy.PUBLIC,
-        videoChannelId: server.videoChannel.id
+        videoChannelId: server.store.channel.id
       }
-      const res = await createVideoPlaylist({ url: server.url, token: server.accessToken, playlistAttrs: attributes })
-
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: res.body.videoPlaylist.id,
-        elementAttrs: { videoId }
-      })
+      const created = await server.playlists.create({ attributes })
+      playlistUUID = created.uuid
+      playlistShortUUID = created.shortUUID
+
+      await server.playlists.addElement({ playlistId: created.id, attributes: { videoId } })
     }
 
     {
+      const videoId = (await remoteServer.videos.upload()).uuid
+
       const attributes = {
-        displayName: 'Johan & Anna Libert musics',
+        displayName: 'Johan & Anna Libert music videos',
         privacy: VideoPlaylistPrivacy.PUBLIC,
-        videoChannelId: server.videoChannel.id
+        videoChannelId: remoteServer.store.channel.id
       }
-      const res = await createVideoPlaylist({ url: server.url, token: server.accessToken, playlistAttrs: attributes })
-
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: res.body.videoPlaylist.id,
-        elementAttrs: { videoId }
-      })
+      const created = await remoteServer.playlists.create({ attributes })
+
+      await remoteServer.playlists.addElement({ playlistId: created.id, attributes: { videoId } })
     }
 
     {
       const attributes = {
         displayName: 'Inspector Lunge playlist',
         privacy: VideoPlaylistPrivacy.PUBLIC,
-        videoChannelId: server.videoChannel.id
+        videoChannelId: server.store.channel.id
       }
-      await createVideoPlaylist({ url: server.url, token: server.accessToken, playlistAttrs: attributes })
+      await server.playlists.create({ attributes })
     }
+
+    await doubleFollow(server, remoteServer)
+
+    command = server.search
   })
 
   it('Should make a simple search and not have results', async function () {
-    const res = await searchVideoPlaylists(server.url, 'abc')
+    const body = await command.searchPlaylists({ search: 'abc' })
 
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   it('Should make a search and have results', async function () {
@@ -87,27 +91,72 @@ describe('Test playlists search', function () {
         start: 0,
         count: 1
       }
-      const res = await advancedVideoPlaylistSearch(server.url, search)
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
+      const body = await command.advancedPlaylistSearch({ search })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
 
-      const playlist: VideoPlaylist = res.body.data[0]
+      const playlist = body.data[0]
       expect(playlist.displayName).to.equal('Dr. Kenzo Tenma hospital videos')
       expect(playlist.url).to.equal(server.url + '/video-playlists/' + playlist.uuid)
     }
 
     {
       const search = {
-        search: 'Anna Livert',
+        search: 'Anna Livert music',
         start: 0,
         count: 1
       }
-      const res = await advancedVideoPlaylistSearch(server.url, search)
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
+      const body = await command.advancedPlaylistSearch({ search })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
+
+      const playlist = body.data[0]
+      expect(playlist.displayName).to.equal('Johan & Anna Libert music videos')
+    }
+  })
+
+  it('Should filter by host', async function () {
+    {
+      const search = { search: 'tenma', host: server.host }
+      const body = await command.advancedPlaylistSearch({ search })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
+
+      const playlist = body.data[0]
+      expect(playlist.displayName).to.equal('Dr. Kenzo Tenma hospital videos')
+    }
+
+    {
+      const search = { search: 'Anna', host: 'example.com' }
+      const body = await command.advancedPlaylistSearch({ search })
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
+    }
+
+    {
+      const search = { search: 'video', host: remoteServer.host }
+      const body = await command.advancedPlaylistSearch({ search })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
+
+      const playlist = body.data[0]
+      expect(playlist.displayName).to.equal('Johan & Anna Libert music videos')
+    }
+  })
+
+  it('Should filter by UUIDs', async function () {
+    for (const uuid of [ playlistUUID, playlistShortUUID ]) {
+      const body = await command.advancedPlaylistSearch({ search: { uuids: [ uuid ] } })
+
+      expect(body.total).to.equal(1)
+      expect(body.data[0].displayName).to.equal('Dr. Kenzo Tenma hospital videos')
+    }
+
+    {
+      const body = await command.advancedPlaylistSearch({ search: { uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } })
 
-      const playlist: VideoPlaylist = res.body.data[0]
-      expect(playlist.displayName).to.equal('Johan & Anna Libert musics')
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
     }
   })
 
@@ -117,12 +166,12 @@ describe('Test playlists search', function () {
       start: 0,
       count: 1
     }
-    const res = await advancedVideoPlaylistSearch(server.url, search)
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    const body = await command.advancedPlaylistSearch({ search })
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   after(async function () {
-    await cleanupTests([ server ])
+    await cleanupTests([ server, remoteServer ])
   })
 })
index 5b8907961d4c8f1e2dfcfbccc5de67ce8646d84c..bd1e4d266eabd6a109442a7eb01bbb056c3a4050 100644 (file)
@@ -2,40 +2,42 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { VideoPrivacy } from '@shared/models'
 import {
-  advancedVideosSearch,
   cleanupTests,
-  createLive,
-  flushAndRunServer,
-  immutableAssign,
-  searchVideo,
-  sendRTMPStreamInVideo,
-  ServerInfo,
+  createSingleServer,
+  doubleFollow,
+  PeerTubeServer,
+  SearchCommand,
   setAccessTokensToServers,
   setDefaultVideoChannel,
   stopFfmpeg,
-  updateCustomSubConfig,
-  uploadVideo,
-  wait,
-  waitUntilLivePublished
-} from '../../../../shared/extra-utils'
-import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions'
+  wait
+} from '@shared/extra-utils'
+import { VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test videos search', function () {
-  let server: ServerInfo = null
+  let server: PeerTubeServer
+  let remoteServer: PeerTubeServer
   let startDate: string
   let videoUUID: string
+  let videoShortUUID: string
+
+  let command: SearchCommand
 
   before(async function () {
-    this.timeout(60000)
+    this.timeout(120000)
 
-    server = await flushAndRunServer(1)
+    const servers = await Promise.all([
+      createSingleServer(1),
+      createSingleServer(2)
+    ])
+    server = servers[0]
+    remoteServer = servers[1]
 
-    await setAccessTokensToServers([ server ])
-    await setDefaultVideoChannel([ server ])
+    await setAccessTokensToServers([ server, remoteServer ])
+    await setDefaultVideoChannel([ server, remoteServer ])
 
     {
       const attributes1 = {
@@ -46,57 +48,50 @@ describe('Test videos search', function () {
         nsfw: false,
         language: 'fr'
       }
-      await uploadVideo(server.url, server.accessToken, attributes1)
+      await server.videos.upload({ attributes: attributes1 })
 
-      const attributes2 = immutableAssign(attributes1, { name: attributes1.name + ' - 2', fixture: 'video_short.mp4' })
-      await uploadVideo(server.url, server.accessToken, attributes2)
+      const attributes2 = { ...attributes1, name: attributes1.name + ' - 2', fixture: 'video_short.mp4' }
+      await server.videos.upload({ attributes: attributes2 })
 
       {
-        const attributes3 = immutableAssign(attributes1, { name: attributes1.name + ' - 3', language: undefined })
-        const res = await uploadVideo(server.url, server.accessToken, attributes3)
-        const videoId = res.body.video.id
-        videoUUID = res.body.video.uuid
-
-        await createVideoCaption({
-          url: server.url,
-          accessToken: server.accessToken,
+        const attributes3 = { ...attributes1, name: attributes1.name + ' - 3', language: undefined }
+        const { id, uuid, shortUUID } = await server.videos.upload({ attributes: attributes3 })
+        videoUUID = uuid
+        videoShortUUID = shortUUID
+
+        await server.captions.add({
           language: 'en',
-          videoId,
+          videoId: id,
           fixture: 'subtitle-good2.vtt',
           mimeType: 'application/octet-stream'
         })
 
-        await createVideoCaption({
-          url: server.url,
-          accessToken: server.accessToken,
+        await server.captions.add({
           language: 'aa',
-          videoId,
+          videoId: id,
           fixture: 'subtitle-good2.vtt',
           mimeType: 'application/octet-stream'
         })
       }
 
-      const attributes4 = immutableAssign(attributes1, { name: attributes1.name + ' - 4', language: 'pl', nsfw: true })
-      await uploadVideo(server.url, server.accessToken, attributes4)
+      const attributes4 = { ...attributes1, name: attributes1.name + ' - 4', language: 'pl', nsfw: true }
+      await server.videos.upload({ attributes: attributes4 })
 
       await wait(1000)
 
       startDate = new Date().toISOString()
 
-      const attributes5 = immutableAssign(attributes1, { name: attributes1.name + ' - 5', licence: 2, language: undefined })
-      await uploadVideo(server.url, server.accessToken, attributes5)
+      const attributes5 = { ...attributes1, name: attributes1.name + ' - 5', licence: 2, language: undefined }
+      await server.videos.upload({ attributes: attributes5 })
 
-      const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2' ] })
-      await uploadVideo(server.url, server.accessToken, attributes6)
+      const attributes6 = { ...attributes1, name: attributes1.name + ' - 6', tags: [ 't1', 't2' ] }
+      await server.videos.upload({ attributes: attributes6 })
 
-      const attributes7 = immutableAssign(attributes1, {
-        name: attributes1.name + ' - 7',
-        originallyPublishedAt: '2019-02-12T09:58:08.286Z'
-      })
-      await uploadVideo(server.url, server.accessToken, attributes7)
+      const attributes7 = { ...attributes1, name: attributes1.name + ' - 7', originallyPublishedAt: '2019-02-12T09:58:08.286Z' }
+      await server.videos.upload({ attributes: attributes7 })
 
-      const attributes8 = immutableAssign(attributes1, { name: attributes1.name + ' - 8', licence: 4 })
-      await uploadVideo(server.url, server.accessToken, attributes8)
+      const attributes8 = { ...attributes1, name: attributes1.name + ' - 8', licence: 4 }
+      await server.videos.upload({ attributes: attributes8 })
     }
 
     {
@@ -107,9 +102,9 @@ describe('Test videos search', function () {
         licence: 2,
         language: 'en'
       }
-      await uploadVideo(server.url, server.accessToken, attributes)
+      await server.videos.upload({ attributes: attributes })
 
-      await uploadVideo(server.url, server.accessToken, immutableAssign(attributes, { name: attributes.name + ' duplicate' }))
+      await server.videos.upload({ attributes: { ...attributes, name: attributes.name + ' duplicate' } })
     }
 
     {
@@ -120,7 +115,7 @@ describe('Test videos search', function () {
         licence: 3,
         language: 'pl'
       }
-      await uploadVideo(server.url, server.accessToken, attributes)
+      await server.videos.upload({ attributes: attributes })
     }
 
     {
@@ -129,11 +124,11 @@ describe('Test videos search', function () {
         tags: [ 'aaaa', 'bbbb', 'cccc' ],
         category: 1
       }
-      await uploadVideo(server.url, server.accessToken, attributes1)
-      await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { category: 2 }))
+      await server.videos.upload({ attributes: attributes1 })
+      await server.videos.upload({ attributes: { ...attributes1, category: 2 } })
 
-      await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { tags: [ 'cccc', 'dddd' ] }))
-      await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { tags: [ 'eeee', 'ffff' ] }))
+      await server.videos.upload({ attributes: { ...attributes1, tags: [ 'cccc', 'dddd' ] } })
+      await server.videos.upload({ attributes: { ...attributes1, tags: [ 'eeee', 'ffff' ] } })
     }
 
     {
@@ -141,24 +136,33 @@ describe('Test videos search', function () {
         name: 'aaaa 2',
         category: 1
       }
-      await uploadVideo(server.url, server.accessToken, attributes1)
-      await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { category: 2 }))
+      await server.videos.upload({ attributes: attributes1 })
+      await server.videos.upload({ attributes: { ...attributes1, category: 2 } })
+    }
+
+    {
+      await remoteServer.videos.upload({ attributes: { name: 'remote video 1' } })
+      await remoteServer.videos.upload({ attributes: { name: 'remote video 2' } })
     }
+
+    await doubleFollow(server, remoteServer)
+
+    command = server.search
   })
 
   it('Should make a simple search and not have results', async function () {
-    const res = await searchVideo(server.url, 'abc')
+    const body = await command.searchVideos({ search: 'abc' })
 
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   it('Should make a simple search and have results', async function () {
-    const res = await searchVideo(server.url, '4444 5555 duplicate')
+    const body = await command.searchVideos({ search: '4444 5555 duplicate' })
 
-    expect(res.body.total).to.equal(2)
+    expect(body.total).to.equal(2)
 
-    const videos = res.body.data
+    const videos = body.data
     expect(videos).to.have.lengthOf(2)
 
     // bestmatch
@@ -167,15 +171,15 @@ describe('Test videos search', function () {
   })
 
   it('Should make a search on tags too, and have results', async function () {
-    const query = {
+    const search = {
       search: 'aaaa',
       categoryOneOf: [ 1 ]
     }
-    const res = await advancedVideosSearch(server.url, query)
+    const body = await command.advancedVideoSearch({ search })
 
-    expect(res.body.total).to.equal(2)
+    expect(body.total).to.equal(2)
 
-    const videos = res.body.data
+    const videos = body.data
     expect(videos).to.have.lengthOf(2)
 
     // bestmatch
@@ -184,14 +188,14 @@ describe('Test videos search', function () {
   })
 
   it('Should filter on tags without a search', async function () {
-    const query = {
+    const search = {
       tagsAllOf: [ 'bbbb' ]
     }
-    const res = await advancedVideosSearch(server.url, query)
+    const body = await command.advancedVideoSearch({ search })
 
-    expect(res.body.total).to.equal(2)
+    expect(body.total).to.equal(2)
 
-    const videos = res.body.data
+    const videos = body.data
     expect(videos).to.have.lengthOf(2)
 
     expect(videos[0].name).to.equal('9999')
@@ -199,14 +203,14 @@ describe('Test videos search', function () {
   })
 
   it('Should filter on category without a search', async function () {
-    const query = {
+    const search = {
       categoryOneOf: [ 3 ]
     }
-    const res = await advancedVideosSearch(server.url, query)
+    const body = await command.advancedVideoSearch({ search: search })
 
-    expect(res.body.total).to.equal(1)
+    expect(body.total).to.equal(1)
 
-    const videos = res.body.data
+    const videos = body.data
     expect(videos).to.have.lengthOf(1)
 
     expect(videos[0].name).to.equal('6666 7777 8888')
@@ -218,11 +222,16 @@ describe('Test videos search', function () {
       categoryOneOf: [ 1 ],
       tagsOneOf: [ 'aAaa', 'ffff' ]
     }
-    const res1 = await advancedVideosSearch(server.url, query)
-    expect(res1.body.total).to.equal(2)
 
-    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsOneOf: [ 'blabla' ] }))
-    expect(res2.body.total).to.equal(0)
+    {
+      const body = await command.advancedVideoSearch({ search: query })
+      expect(body.total).to.equal(2)
+    }
+
+    {
+      const body = await command.advancedVideoSearch({ search: { ...query, tagsOneOf: [ 'blabla' ] } })
+      expect(body.total).to.equal(0)
+    }
   })
 
   it('Should search by tags (all of)', async function () {
@@ -231,14 +240,21 @@ describe('Test videos search', function () {
       categoryOneOf: [ 1 ],
       tagsAllOf: [ 'CCcc' ]
     }
-    const res1 = await advancedVideosSearch(server.url, query)
-    expect(res1.body.total).to.equal(2)
 
-    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'blAbla' ] }))
-    expect(res2.body.total).to.equal(0)
+    {
+      const body = await command.advancedVideoSearch({ search: query })
+      expect(body.total).to.equal(2)
+    }
+
+    {
+      const body = await command.advancedVideoSearch({ search: { ...query, tagsAllOf: [ 'blAbla' ] } })
+      expect(body.total).to.equal(0)
+    }
 
-    const res3 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'bbbb', 'CCCC' ] }))
-    expect(res3.body.total).to.equal(1)
+    {
+      const body = await command.advancedVideoSearch({ search: { ...query, tagsAllOf: [ 'bbbb', 'CCCC' ] } })
+      expect(body.total).to.equal(1)
+    }
   })
 
   it('Should search by category', async function () {
@@ -246,12 +262,17 @@ describe('Test videos search', function () {
       search: '6666',
       categoryOneOf: [ 3 ]
     }
-    const res1 = await advancedVideosSearch(server.url, query)
-    expect(res1.body.total).to.equal(1)
-    expect(res1.body.data[0].name).to.equal('6666 7777 8888')
 
-    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { categoryOneOf: [ 2 ] }))
-    expect(res2.body.total).to.equal(0)
+    {
+      const body = await command.advancedVideoSearch({ search: query })
+      expect(body.total).to.equal(1)
+      expect(body.data[0].name).to.equal('6666 7777 8888')
+    }
+
+    {
+      const body = await command.advancedVideoSearch({ search: { ...query, categoryOneOf: [ 2 ] } })
+      expect(body.total).to.equal(0)
+    }
   })
 
   it('Should search by licence', async function () {
@@ -259,13 +280,18 @@ describe('Test videos search', function () {
       search: '4444 5555',
       licenceOneOf: [ 2 ]
     }
-    const res1 = await advancedVideosSearch(server.url, query)
-    expect(res1.body.total).to.equal(2)
-    expect(res1.body.data[0].name).to.equal('3333 4444 5555')
-    expect(res1.body.data[1].name).to.equal('3333 4444 5555 duplicate')
 
-    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { licenceOneOf: [ 3 ] }))
-    expect(res2.body.total).to.equal(0)
+    {
+      const body = await command.advancedVideoSearch({ search: query })
+      expect(body.total).to.equal(2)
+      expect(body.data[0].name).to.equal('3333 4444 5555')
+      expect(body.data[1].name).to.equal('3333 4444 5555 duplicate')
+    }
+
+    {
+      const body = await command.advancedVideoSearch({ search: { ...query, licenceOneOf: [ 3 ] } })
+      expect(body.total).to.equal(0)
+    }
   })
 
   it('Should search by languages', async function () {
@@ -275,23 +301,23 @@ describe('Test videos search', function () {
     }
 
     {
-      const res = await advancedVideosSearch(server.url, query)
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3')
-      expect(res.body.data[1].name).to.equal('1111 2222 3333 - 4')
+      const body = await command.advancedVideoSearch({ search: query })
+      expect(body.total).to.equal(2)
+      expect(body.data[0].name).to.equal('1111 2222 3333 - 3')
+      expect(body.data[1].name).to.equal('1111 2222 3333 - 4')
     }
 
     {
-      const res = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'pl', 'en', '_unknown' ] }))
-      expect(res.body.total).to.equal(3)
-      expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3')
-      expect(res.body.data[1].name).to.equal('1111 2222 3333 - 4')
-      expect(res.body.data[2].name).to.equal('1111 2222 3333 - 5')
+      const body = await command.advancedVideoSearch({ search: { ...query, languageOneOf: [ 'pl', 'en', '_unknown' ] } })
+      expect(body.total).to.equal(3)
+      expect(body.data[0].name).to.equal('1111 2222 3333 - 3')
+      expect(body.data[1].name).to.equal('1111 2222 3333 - 4')
+      expect(body.data[2].name).to.equal('1111 2222 3333 - 5')
     }
 
     {
-      const res = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'eo' ] }))
-      expect(res.body.total).to.equal(0)
+      const body = await command.advancedVideoSearch({ search: { ...query, languageOneOf: [ 'eo' ] } })
+      expect(body.total).to.equal(0)
     }
   })
 
@@ -301,10 +327,10 @@ describe('Test videos search', function () {
       startDate
     }
 
-    const res = await advancedVideosSearch(server.url, query)
-    expect(res.body.total).to.equal(4)
+    const body = await command.advancedVideoSearch({ search: query })
+    expect(body.total).to.equal(4)
 
-    const videos = res.body.data
+    const videos = body.data
     expect(videos[0].name).to.equal('1111 2222 3333 - 5')
     expect(videos[1].name).to.equal('1111 2222 3333 - 6')
     expect(videos[2].name).to.equal('1111 2222 3333 - 7')
@@ -320,10 +346,10 @@ describe('Test videos search', function () {
       licenceOneOf: [ 1, 4 ]
     }
 
-    const res = await advancedVideosSearch(server.url, query)
-    expect(res.body.total).to.equal(4)
+    const body = await command.advancedVideoSearch({ search: query })
+    expect(body.total).to.equal(4)
 
-    const videos = res.body.data
+    const videos = body.data
     expect(videos[0].name).to.equal('1111 2222 3333')
     expect(videos[1].name).to.equal('1111 2222 3333 - 6')
     expect(videos[2].name).to.equal('1111 2222 3333 - 7')
@@ -340,10 +366,10 @@ describe('Test videos search', function () {
       sort: '-name'
     }
 
-    const res = await advancedVideosSearch(server.url, query)
-    expect(res.body.total).to.equal(4)
+    const body = await command.advancedVideoSearch({ search: query })
+    expect(body.total).to.equal(4)
 
-    const videos = res.body.data
+    const videos = body.data
     expect(videos[0].name).to.equal('1111 2222 3333 - 8')
     expect(videos[1].name).to.equal('1111 2222 3333 - 7')
     expect(videos[2].name).to.equal('1111 2222 3333 - 6')
@@ -362,10 +388,10 @@ describe('Test videos search', function () {
       count: 1
     }
 
-    const res = await advancedVideosSearch(server.url, query)
-    expect(res.body.total).to.equal(4)
+    const body = await command.advancedVideoSearch({ search: query })
+    expect(body.total).to.equal(4)
 
-    const videos = res.body.data
+    const videos = body.data
     expect(videos[0].name).to.equal('1111 2222 3333 - 8')
   })
 
@@ -381,10 +407,10 @@ describe('Test videos search', function () {
       count: 1
     }
 
-    const res = await advancedVideosSearch(server.url, query)
-    expect(res.body.total).to.equal(4)
+    const body = await command.advancedVideoSearch({ search: query })
+    expect(body.total).to.equal(4)
 
-    const videos = res.body.data
+    const videos = body.data
     expect(videos[0].name).to.equal('1111 2222 3333')
   })
 
@@ -398,99 +424,140 @@ describe('Test videos search', function () {
     }
 
     {
-      const query = immutableAssign(baseQuery, { originallyPublishedStartDate: '2019-02-11T09:58:08.286Z' })
-      const res = await advancedVideosSearch(server.url, query)
+      const query = { ...baseQuery, originallyPublishedStartDate: '2019-02-11T09:58:08.286Z' }
+      const body = await command.advancedVideoSearch({ search: query })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data[0].name).to.equal('1111 2222 3333 - 7')
+      expect(body.total).to.equal(1)
+      expect(body.data[0].name).to.equal('1111 2222 3333 - 7')
     }
 
     {
-      const query = immutableAssign(baseQuery, { originallyPublishedEndDate: '2019-03-11T09:58:08.286Z' })
-      const res = await advancedVideosSearch(server.url, query)
+      const query = { ...baseQuery, originallyPublishedEndDate: '2019-03-11T09:58:08.286Z' }
+      const body = await command.advancedVideoSearch({ search: query })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data[0].name).to.equal('1111 2222 3333 - 7')
+      expect(body.total).to.equal(1)
+      expect(body.data[0].name).to.equal('1111 2222 3333 - 7')
     }
 
     {
-      const query = immutableAssign(baseQuery, { originallyPublishedEndDate: '2019-01-11T09:58:08.286Z' })
-      const res = await advancedVideosSearch(server.url, query)
+      const query = { ...baseQuery, originallyPublishedEndDate: '2019-01-11T09:58:08.286Z' }
+      const body = await command.advancedVideoSearch({ search: query })
 
-      expect(res.body.total).to.equal(0)
+      expect(body.total).to.equal(0)
     }
 
     {
-      const query = immutableAssign(baseQuery, { originallyPublishedStartDate: '2019-03-11T09:58:08.286Z' })
-      const res = await advancedVideosSearch(server.url, query)
+      const query = { ...baseQuery, originallyPublishedStartDate: '2019-03-11T09:58:08.286Z' }
+      const body = await command.advancedVideoSearch({ search: query })
 
-      expect(res.body.total).to.equal(0)
+      expect(body.total).to.equal(0)
     }
 
     {
-      const query = immutableAssign(baseQuery, {
+      const query = {
+        ...baseQuery,
         originallyPublishedStartDate: '2019-01-11T09:58:08.286Z',
         originallyPublishedEndDate: '2019-01-10T09:58:08.286Z'
-      })
-      const res = await advancedVideosSearch(server.url, query)
+      }
+      const body = await command.advancedVideoSearch({ search: query })
 
-      expect(res.body.total).to.equal(0)
+      expect(body.total).to.equal(0)
     }
 
     {
-      const query = immutableAssign(baseQuery, {
+      const query = {
+        ...baseQuery,
         originallyPublishedStartDate: '2019-01-11T09:58:08.286Z',
         originallyPublishedEndDate: '2019-04-11T09:58:08.286Z'
-      })
-      const res = await advancedVideosSearch(server.url, query)
+      }
+      const body = await command.advancedVideoSearch({ search: query })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data[0].name).to.equal('1111 2222 3333 - 7')
+      expect(body.total).to.equal(1)
+      expect(body.data[0].name).to.equal('1111 2222 3333 - 7')
     }
   })
 
   it('Should search by UUID', async function () {
     const search = videoUUID
-    const res = await advancedVideosSearch(server.url, { search })
+    const body = await command.advancedVideoSearch({ search: { search } })
+
+    expect(body.total).to.equal(1)
+    expect(body.data[0].name).to.equal('1111 2222 3333 - 3')
+  })
+
+  it('Should filter by UUIDs', async function () {
+    for (const uuid of [ videoUUID, videoShortUUID ]) {
+      const body = await command.advancedVideoSearch({ search: { uuids: [ uuid ] } })
+
+      expect(body.total).to.equal(1)
+      expect(body.data[0].name).to.equal('1111 2222 3333 - 3')
+    }
+
+    {
+      const body = await command.advancedVideoSearch({ search: { uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } })
+
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
+    }
+  })
+
+  it('Should search by host', async function () {
+    {
+      const body = await command.advancedVideoSearch({ search: { search: '6666 7777 8888', host: server.host } })
+      expect(body.total).to.equal(1)
+      expect(body.data[0].name).to.equal('6666 7777 8888')
+    }
 
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3')
+    {
+      const body = await command.advancedVideoSearch({ search: { search: '1111', host: 'example.com' } })
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
+    }
+
+    {
+      const body = await command.advancedVideoSearch({ search: { search: 'remote', host: remoteServer.host } })
+      expect(body.total).to.equal(2)
+      expect(body.data).to.have.lengthOf(2)
+      expect(body.data[0].name).to.equal('remote video 1')
+      expect(body.data[1].name).to.equal('remote video 2')
+    }
   })
 
   it('Should search by live', async function () {
-    this.timeout(30000)
+    this.timeout(60000)
 
     {
-      const options = {
+      const newConfig = {
         search: {
           searchIndex: { enabled: false }
         },
         live: { enabled: true }
       }
-      await updateCustomSubConfig(server.url, server.accessToken, options)
+      await server.config.updateCustomSubConfig({ newConfig })
     }
 
     {
-      const res = await advancedVideosSearch(server.url, { isLive: true })
+      const body = await command.advancedVideoSearch({ search: { isLive: true } })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
     }
 
     {
-      const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: server.videoChannel.id }
-      const resLive = await createLive(server.url, server.accessToken, liveOptions)
-      const liveVideoId = resLive.body.video.uuid
+      const liveCommand = server.live
+
+      const liveAttributes = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: server.store.channel.id }
+      const live = await liveCommand.create({ fields: liveAttributes })
 
-      const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId)
-      await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
+      const ffmpegCommand = await liveCommand.sendRTMPStreamInVideo({ videoId: live.id })
+      await liveCommand.waitUntilPublished({ videoId: live.id })
 
-      const res = await advancedVideosSearch(server.url, { isLive: true })
+      const body = await command.advancedVideoSearch({ search: { isLive: true } })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data[0].name).to.equal('live')
+      expect(body.total).to.equal(1)
+      expect(body.data[0].name).to.equal('live')
 
-      await stopFfmpeg(command)
+      await stopFfmpeg(ffmpegCommand)
     }
   })
 
index 1519b263fb0eb54dac0c53d3b69b817a83fd800b..ce7b519252517f490bcf891aa31cce117798e8ad 100644 (file)
@@ -3,64 +3,45 @@
 import 'mocha'
 import * as chai from 'chai'
 import {
-  acceptFollower,
   cleanupTests,
-  flushAndRunMultipleServers,
+  createMultipleServers,
   MockInstancesIndex,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  unfollow,
-  updateCustomSubConfig,
-  wait
-} from '../../../../shared/extra-utils/index'
-import { follow, getFollowersListPaginationAndSort, getFollowingListPaginationAndSort } from '../../../../shared/extra-utils/server/follows'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { ActorFollow } from '../../../../shared/models/actors'
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
 
 const expect = chai.expect
 
-async function checkFollow (follower: ServerInfo, following: ServerInfo, exists: boolean) {
+async function checkFollow (follower: PeerTubeServer, following: PeerTubeServer, exists: boolean) {
   {
-    const res = await getFollowersListPaginationAndSort({ url: following.url, start: 0, count: 5, sort: '-createdAt' })
-    const follows = res.body.data as ActorFollow[]
+    const body = await following.follows.getFollowers({ start: 0, count: 5, sort: '-createdAt' })
+    const follow = body.data.find(f => f.follower.host === follower.host && f.state === 'accepted')
 
-    const follow = follows.find(f => {
-      return f.follower.host === follower.host && f.state === 'accepted'
-    })
-
-    if (exists === true) {
-      expect(follow).to.exist
-    } else {
-      expect(follow).to.be.undefined
-    }
+    if (exists === true) expect(follow).to.exist
+    else expect(follow).to.be.undefined
   }
 
   {
-    const res = await getFollowingListPaginationAndSort({ url: follower.url, start: 0, count: 5, sort: '-createdAt' })
-    const follows = res.body.data as ActorFollow[]
-
-    const follow = follows.find(f => {
-      return f.following.host === following.host && f.state === 'accepted'
-    })
+    const body = await follower.follows.getFollowings({ start: 0, count: 5, sort: '-createdAt' })
+    const follow = body.data.find(f => f.following.host === following.host && f.state === 'accepted')
 
-    if (exists === true) {
-      expect(follow).to.exist
-    } else {
-      expect(follow).to.be.undefined
-    }
+    if (exists === true) expect(follow).to.exist
+    else expect(follow).to.be.undefined
   }
 }
 
-async function server1Follows2 (servers: ServerInfo[]) {
-  await follow(servers[0].url, [ servers[1].host ], servers[0].accessToken)
+async function server1Follows2 (servers: PeerTubeServer[]) {
+  await servers[0].follows.follow({ hosts: [ servers[1].host ] })
 
   await waitJobs(servers)
 }
 
-async function resetFollows (servers: ServerInfo[]) {
+async function resetFollows (servers: PeerTubeServer[]) {
   try {
-    await unfollow(servers[0].url, servers[0].accessToken, servers[1])
-    await unfollow(servers[1].url, servers[1].accessToken, servers[0])
+    await servers[0].follows.unfollow({ target: servers[1] })
+    await servers[1].follows.unfollow({ target: servers[0] })
   } catch { /* empty */
   }
 
@@ -71,12 +52,12 @@ async function resetFollows (servers: ServerInfo[]) {
 }
 
 describe('Test auto follows', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
 
   before(async function () {
     this.timeout(30000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -105,7 +86,7 @@ describe('Test auto follows', function () {
           }
         }
       }
-      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
+      await servers[1].config.updateCustomSubConfig({ newConfig: config })
 
       await server1Follows2(servers)
 
@@ -130,14 +111,14 @@ describe('Test auto follows', function () {
           }
         }
       }
-      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
+      await servers[1].config.updateCustomSubConfig({ newConfig: config })
 
       await server1Follows2(servers)
 
       await checkFollow(servers[0], servers[1], false)
       await checkFollow(servers[1], servers[0], false)
 
-      await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@' + servers[0].host)
+      await servers[1].follows.acceptFollower({ follower: 'peertube@' + servers[0].host })
       await waitJobs(servers)
 
       await checkFollow(servers[0], servers[1], true)
@@ -147,7 +128,7 @@ describe('Test auto follows', function () {
 
       config.followings.instance.autoFollowBack.enabled = false
       config.followers.instance.manualApproval = false
-      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
+      await servers[1].config.updateCustomSubConfig({ newConfig: config })
     })
   })
 
@@ -184,7 +165,7 @@ describe('Test auto follows', function () {
           }
         }
       }
-      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+      await servers[0].config.updateCustomSubConfig({ newConfig: config })
 
       await wait(5000)
       await waitJobs(servers)
index 80fa7fce67779f4e14ba40e35f984edc0d576b5e..16cbcd5c38f5887134f8816f1b14585c361cc1f9 100644 (file)
@@ -2,91 +2,83 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { Video, VideoComment } from '@shared/models'
 import {
-  addVideoCommentReply,
-  addVideoCommentThread,
-  bulkRemoveCommentsOf,
+  BulkCommand,
   cleanupTests,
-  createUser,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getVideoCommentThreads,
-  getVideosList,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  uploadVideo,
-  userLogin,
   waitJobs
-} from '../../../../shared/extra-utils/index'
+} from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test bulk actions', function () {
   const commentsUser3: { videoId: number, commentId: number }[] = []
 
-  let servers: ServerInfo[] = []
-  let user1AccessToken: string
-  let user2AccessToken: string
-  let user3AccessToken: string
+  let servers: PeerTubeServer[] = []
+  let user1Token: string
+  let user2Token: string
+  let user3Token: string
+
+  let bulkCommand: BulkCommand
 
   before(async function () {
     this.timeout(30000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
 
     {
       const user = { username: 'user1', password: 'password' }
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
+      await servers[0].users.create({ username: user.username, password: user.password })
 
-      user1AccessToken = await userLogin(servers[0], user)
+      user1Token = await servers[0].login.getAccessToken(user)
     }
 
     {
       const user = { username: 'user2', password: 'password' }
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
+      await servers[0].users.create({ username: user.username, password: user.password })
 
-      user2AccessToken = await userLogin(servers[0], user)
+      user2Token = await servers[0].login.getAccessToken(user)
     }
 
     {
       const user = { username: 'user3', password: 'password' }
-      await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
+      await servers[1].users.create({ username: user.username, password: user.password })
 
-      user3AccessToken = await userLogin(servers[1], user)
+      user3Token = await servers[1].login.getAccessToken(user)
     }
 
     await doubleFollow(servers[0], servers[1])
+
+    bulkCommand = new BulkCommand(servers[0])
   })
 
   describe('Bulk remove comments', function () {
     async function checkInstanceCommentsRemoved () {
       {
-        const res = await getVideosList(servers[0].url)
-        const videos = res.body.data as Video[]
+        const { data } = await servers[0].videos.list()
 
         // Server 1 should not have these comments anymore
-        for (const video of videos) {
-          const resThreads = await getVideoCommentThreads(servers[0].url, video.id, 0, 10)
-          const comments = resThreads.body.data as VideoComment[]
-          const comment = comments.find(c => c.text === 'comment by user 3')
+        for (const video of data) {
+          const { data } = await servers[0].comments.listThreads({ videoId: video.id })
+          const comment = data.find(c => c.text === 'comment by user 3')
 
           expect(comment).to.not.exist
         }
       }
 
       {
-        const res = await getVideosList(servers[1].url)
-        const videos = res.body.data as Video[]
+        const { data } = await servers[1].videos.list()
 
         // Server 1 should not have these comments on videos of server 1
-        for (const video of videos) {
-          const resThreads = await getVideoCommentThreads(servers[1].url, video.id, 0, 10)
-          const comments = resThreads.body.data as VideoComment[]
-          const comment = comments.find(c => c.text === 'comment by user 3')
+        for (const video of data) {
+          const { data } = await servers[1].comments.listThreads({ videoId: video.id })
+          const comment = data.find(c => c.text === 'comment by user 3')
 
           if (video.account.host === 'localhost:' + servers[0].port) {
             expect(comment).to.not.exist
@@ -100,30 +92,31 @@ describe('Test bulk actions', function () {
     before(async function () {
       this.timeout(120000)
 
-      await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 1 server 1' })
-      await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 2 server 1' })
-      await uploadVideo(servers[0].url, user1AccessToken, { name: 'video 3 server 1' })
+      await servers[0].videos.upload({ attributes: { name: 'video 1 server 1' } })
+      await servers[0].videos.upload({ attributes: { name: 'video 2 server 1' } })
+      await servers[0].videos.upload({ token: user1Token, attributes: { name: 'video 3 server 1' } })
 
-      await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 server 2' })
+      await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
 
       await waitJobs(servers)
 
       {
-        const res = await getVideosList(servers[0].url)
-        for (const video of res.body.data) {
-          await addVideoCommentThread(servers[0].url, servers[0].accessToken, video.id, 'comment by root server 1')
-          await addVideoCommentThread(servers[0].url, user1AccessToken, video.id, 'comment by user 1')
-          await addVideoCommentThread(servers[0].url, user2AccessToken, video.id, 'comment by user 2')
+        const { data } = await servers[0].videos.list()
+        for (const video of data) {
+          await servers[0].comments.createThread({ videoId: video.id, text: 'comment by root server 1' })
+          await servers[0].comments.createThread({ token: user1Token, videoId: video.id, text: 'comment by user 1' })
+          await servers[0].comments.createThread({ token: user2Token, videoId: video.id, text: 'comment by user 2' })
         }
       }
 
       {
-        const res = await getVideosList(servers[1].url)
-        for (const video of res.body.data) {
-          await addVideoCommentThread(servers[1].url, servers[1].accessToken, video.id, 'comment by root server 2')
+        const { data } = await servers[1].videos.list()
 
-          const res = await addVideoCommentThread(servers[1].url, user3AccessToken, video.id, 'comment by user 3')
-          commentsUser3.push({ videoId: video.id, commentId: res.body.comment.id })
+        for (const video of data) {
+          await servers[1].comments.createThread({ videoId: video.id, text: 'comment by root server 2' })
+
+          const comment = await servers[1].comments.createThread({ token: user3Token, videoId: video.id, text: 'comment by user 3' })
+          commentsUser3.push({ videoId: video.id, commentId: comment.id })
         }
       }
 
@@ -133,9 +126,8 @@ describe('Test bulk actions', function () {
     it('Should delete comments of an account on my videos', async function () {
       this.timeout(60000)
 
-      await bulkRemoveCommentsOf({
-        url: servers[0].url,
-        token: user1AccessToken,
+      await bulkCommand.removeCommentsOf({
+        token: user1Token,
         attributes: {
           accountName: 'user2',
           scope: 'my-videos'
@@ -145,18 +137,14 @@ describe('Test bulk actions', function () {
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        for (const video of res.body.data) {
-          const resThreads = await getVideoCommentThreads(server.url, video.id, 0, 10)
-          const comments = resThreads.body.data as VideoComment[]
-          const comment = comments.find(c => c.text === 'comment by user 2')
+        for (const video of data) {
+          const { data } = await server.comments.listThreads({ videoId: video.id })
+          const comment = data.find(c => c.text === 'comment by user 2')
 
-          if (video.name === 'video 3 server 1') {
-            expect(comment).to.not.exist
-          } else {
-            expect(comment).to.exist
-          }
+          if (video.name === 'video 3 server 1') expect(comment).to.not.exist
+          else expect(comment).to.exist
         }
       }
     })
@@ -164,9 +152,7 @@ describe('Test bulk actions', function () {
     it('Should delete comments of an account on the instance', async function () {
       this.timeout(60000)
 
-      await bulkRemoveCommentsOf({
-        url: servers[0].url,
-        token: servers[0].accessToken,
+      await bulkCommand.removeCommentsOf({
         attributes: {
           accountName: 'user3@localhost:' + servers[1].port,
           scope: 'instance'
@@ -182,7 +168,12 @@ describe('Test bulk actions', function () {
       this.timeout(60000)
 
       for (const obj of commentsUser3) {
-        await addVideoCommentReply(servers[1].url, user3AccessToken, obj.videoId, obj.commentId, 'comment by user 3 bis')
+        await servers[1].comments.addReply({
+          token: user3Token,
+          videoId: obj.videoId,
+          toCommentId: obj.commentId,
+          text: 'comment by user 3 bis'
+        })
       }
 
       await waitJobs(servers)
index 19bf9582c68e3fdd08ad4feda1e824d934d5c3fb..fd61e95df2d9060a16b426fba9c85f9a17a5712d 100644 (file)
@@ -2,31 +2,20 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { About } from '../../../../shared/models/server/about.model'
-import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
 import {
   cleanupTests,
-  deleteCustomConfig,
-  flushAndRunServer,
-  getAbout,
-  getConfig,
-  getCustomConfig,
+  createSingleServer,
   killallServers,
   makeGetRequest,
   parallelTests,
-  registerUser,
-  reRunServer,
-  ServerInfo,
-  setAccessTokensToServers,
-  updateCustomConfig,
-  uploadVideo
-} from '../../../../shared/extra-utils'
-import { ServerConfig } from '../../../../shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { CustomConfig, HttpStatusCode } from '@shared/models'
 
 const expect = chai.expect
 
-function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
+function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
   expect(data.instance.name).to.equal('PeerTube')
   expect(data.instance.shortDescription).to.equal(
     'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.'
@@ -213,18 +202,17 @@ function checkUpdatedConfig (data: CustomConfig) {
 }
 
 describe('Test config', function () {
-  let server = null
+  let server: PeerTubeServer = null
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
   })
 
   it('Should have a correct config on a server with registration enabled', async function () {
-    const res = await getConfig(server.url)
-    const data: ServerConfig = res.body
+    const data = await server.config.getConfig()
 
     expect(data.signup.allowed).to.be.true
   })
@@ -233,35 +221,32 @@ describe('Test config', function () {
     this.timeout(5000)
 
     await Promise.all([
-      registerUser(server.url, 'user1', 'super password'),
-      registerUser(server.url, 'user2', 'super password'),
-      registerUser(server.url, 'user3', 'super password')
+      server.users.register({ username: 'user1' }),
+      server.users.register({ username: 'user2' }),
+      server.users.register({ username: 'user3' })
     ])
 
-    const res = await getConfig(server.url)
-    const data: ServerConfig = res.body
+    const data = await server.config.getConfig()
 
     expect(data.signup.allowed).to.be.false
   })
 
   it('Should have the correct video allowed extensions', async function () {
-    const res = await getConfig(server.url)
-    const data: ServerConfig = res.body
+    const data = await server.config.getConfig()
 
     expect(data.video.file.extensions).to.have.lengthOf(3)
     expect(data.video.file.extensions).to.contain('.mp4')
     expect(data.video.file.extensions).to.contain('.webm')
     expect(data.video.file.extensions).to.contain('.ogv')
 
-    await uploadVideo(server.url, server.accessToken, { fixture: 'video_short.mkv' }, HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
-    await uploadVideo(server.url, server.accessToken, { fixture: 'sample.ogg' }, HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
+    await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
+    await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
 
     expect(data.contactForm.enabled).to.be.true
   })
 
   it('Should get the customized configuration', async function () {
-    const res = await getCustomConfig(server.url, server.accessToken)
-    const data = res.body as CustomConfig
+    const data = await server.config.getCustomConfig()
 
     checkInitialConfig(server, data)
   })
@@ -438,19 +423,16 @@ describe('Test config', function () {
         }
       }
     }
-    await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
-
-    const res = await getCustomConfig(server.url, server.accessToken)
-    const data = res.body
+    await server.config.updateCustomConfig({ newCustomConfig })
 
+    const data = await server.config.getCustomConfig()
     checkUpdatedConfig(data)
   })
 
   it('Should have the correct updated video allowed extensions', async function () {
     this.timeout(10000)
 
-    const res = await getConfig(server.url)
-    const data: ServerConfig = res.body
+    const data = await server.config.getConfig()
 
     expect(data.video.file.extensions).to.have.length.above(4)
     expect(data.video.file.extensions).to.contain('.mp4')
@@ -463,26 +445,24 @@ describe('Test config', function () {
     expect(data.video.file.extensions).to.contain('.ogg')
     expect(data.video.file.extensions).to.contain('.flac')
 
-    await uploadVideo(server.url, server.accessToken, { fixture: 'video_short.mkv' }, HttpStatusCode.OK_200)
-    await uploadVideo(server.url, server.accessToken, { fixture: 'sample.ogg' }, HttpStatusCode.OK_200)
+    await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.OK_200 })
+    await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.OK_200 })
   })
 
   it('Should have the configuration updated after a restart', async function () {
     this.timeout(10000)
 
-    killallServers([ server ])
+    await killallServers([ server ])
 
-    await reRunServer(server)
+    await server.run()
 
-    const res = await getCustomConfig(server.url, server.accessToken)
-    const data = res.body
+    const data = await server.config.getCustomConfig()
 
     checkUpdatedConfig(data)
   })
 
   it('Should fetch the about information', async function () {
-    const res = await getAbout(server.url)
-    const data: About = res.body
+    const data = await server.config.getAbout()
 
     expect(data.instance.name).to.equal('PeerTube updated')
     expect(data.instance.shortDescription).to.equal('my short description')
@@ -504,11 +484,9 @@ describe('Test config', function () {
   it('Should remove the custom configuration', async function () {
     this.timeout(10000)
 
-    await deleteCustomConfig(server.url, server.accessToken)
-
-    const res = await getCustomConfig(server.url, server.accessToken)
-    const data = res.body
+    await server.config.deleteCustomConfig()
 
+    const data = await server.config.getCustomConfig()
     checkInitialConfig(server, data)
   })
 
@@ -519,26 +497,26 @@ describe('Test config', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/api/v1/config',
-        statusCodeExpected: 200
+        expectedStatus: 200
       })
 
       expect(res.headers['x-frame-options']).to.exist
     }
 
-    killallServers([ server ])
+    await killallServers([ server ])
 
     const config = {
       security: {
         frameguard: { enabled: false }
       }
     }
-    server = await reRunServer(server, config)
+    await server.run(config)
 
     {
       const res = await makeGetRequest({
         url: server.url,
         path: '/api/v1/config',
-        statusCodeExpected: 200
+        expectedStatus: 200
       })
 
       expect(res.headers['x-frame-options']).to.not.exist
index 8851ad55ed14357d4f4042d996dc726f62708cf0..c555661adce5ae137bd3f0c295b317fb0c2eadc7 100644 (file)
@@ -1,18 +1,25 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { cleanupTests, flushAndRunServer, ServerInfo, setAccessTokensToServers, wait } from '../../../../shared/extra-utils'
-import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { sendContactForm } from '../../../../shared/extra-utils/server/contact-form'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import * as chai from 'chai'
+import {
+  cleanupTests,
+  ContactFormCommand,
+  createSingleServer,
+  MockSmtpServer,
+  PeerTubeServer,
+  setAccessTokensToServers,
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test contact form', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   const emails: object[] = []
+  let command: ContactFormCommand
 
   before(async function () {
     this.timeout(30000)
@@ -25,15 +32,16 @@ describe('Test contact form', function () {
         port
       }
     }
-    server = await flushAndRunServer(1, overrideConfig)
+    server = await createSingleServer(1, overrideConfig)
     await setAccessTokensToServers([ server ])
+
+    command = server.contactForm
   })
 
   it('Should send a contact form', async function () {
     this.timeout(10000)
 
-    await sendContactForm({
-      url: server.url,
+    await command.send({
       fromEmail: 'toto@example.com',
       body: 'my super message',
       subject: 'my subject',
@@ -58,16 +66,14 @@ describe('Test contact form', function () {
 
     await wait(1000)
 
-    await sendContactForm({
-      url: server.url,
+    await command.send({
       fromEmail: 'toto@example.com',
       body: 'my super message',
       subject: 'my subject',
       fromName: 'Super toto'
     })
 
-    await sendContactForm({
-      url: server.url,
+    await command.send({
       fromEmail: 'toto@example.com',
       body: 'my super message',
       fromName: 'Super toto',
@@ -79,8 +85,7 @@ describe('Test contact form', function () {
   it('Should be able to send another contact form after a while', async function () {
     await wait(1000)
 
-    await sendContactForm({
-      url: server.url,
+    await command.send({
       fromEmail: 'toto@example.com',
       fromName: 'Super toto',
       subject: 'my subject',
index 92768d9dfca59c26f34d401b0a1668b6bbe7aa26..5f97edbc28d2040acf519201a9b0dd9f0c36c522 100644 (file)
@@ -2,37 +2,18 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import {
-  addVideoToBlacklist,
-  askResetPassword,
-  askSendVerifyEmail,
-  blockUser,
-  cleanupTests,
-  createUser,
-  flushAndRunServer,
-  removeVideoFromBlacklist,
-  reportAbuse,
-  resetPassword,
-  ServerInfo,
-  setAccessTokensToServers,
-  unblockUser,
-  uploadVideo,
-  userLogin,
-  verifyEmail
-} from '../../../../shared/extra-utils'
-import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { cleanupTests, createSingleServer, MockSmtpServer, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test emails', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userId: number
   let userId2: number
   let userAccessToken: string
 
-  let videoUUID: string
+  let videoShortUUID: string
   let videoId: number
 
   let videoUserUUID: string
@@ -58,31 +39,29 @@ describe('Test emails', function () {
         port: emailPort
       }
     }
-    server = await flushAndRunServer(1, overrideConfig)
+    server = await createSingleServer(1, overrideConfig)
     await setAccessTokensToServers([ server ])
 
     {
-      const res = await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-      userId = res.body.user.id
+      const created = await server.users.create({ username: user.username, password: user.password })
+      userId = created.id
 
-      userAccessToken = await userLogin(server, user)
+      userAccessToken = await server.login.getAccessToken(user)
     }
 
     {
-      const attributes = {
-        name: 'my super user video'
-      }
-      const res = await uploadVideo(server.url, userAccessToken, attributes)
-      videoUserUUID = res.body.video.uuid
+      const attributes = { name: 'my super user video' }
+      const { uuid } = await server.videos.upload({ token: userAccessToken, attributes })
+      videoUserUUID = uuid
     }
 
     {
       const attributes = {
         name: 'my super name'
       }
-      const res = await uploadVideo(server.url, server.accessToken, attributes)
-      videoUUID = res.body.video.uuid
-      videoId = res.body.video.id
+      const { shortUUID, id } = await server.videos.upload({ attributes })
+      videoShortUUID = shortUUID
+      videoId = id
     }
   })
 
@@ -91,7 +70,7 @@ describe('Test emails', function () {
     it('Should ask to reset the password', async function () {
       this.timeout(10000)
 
-      await askResetPassword(server.url, 'user_1@example.com')
+      await server.users.askResetPassword({ email: 'user_1@example.com' })
 
       await waitJobs(server)
       expect(emails).to.have.lengthOf(1)
@@ -117,34 +96,40 @@ describe('Test emails', function () {
     })
 
     it('Should not reset the password with an invalid verification string', async function () {
-      await resetPassword(server.url, userId, verificationString + 'b', 'super_password2', HttpStatusCode.FORBIDDEN_403)
+      await server.users.resetPassword({
+        userId,
+        verificationString: verificationString + 'b',
+        password: 'super_password2',
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
     })
 
     it('Should reset the password', async function () {
-      await resetPassword(server.url, userId, verificationString, 'super_password2')
+      await server.users.resetPassword({ userId, verificationString, password: 'super_password2' })
     })
 
     it('Should not reset the password with the same verification string', async function () {
-      await resetPassword(server.url, userId, verificationString, 'super_password3', HttpStatusCode.FORBIDDEN_403)
+      await server.users.resetPassword({
+        userId,
+        verificationString,
+        password: 'super_password3',
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
     })
 
     it('Should login with this new password', async function () {
       user.password = 'super_password2'
 
-      await userLogin(server, user)
+      await server.login.getAccessToken(user)
     })
   })
 
   describe('When creating a user without password', function () {
+
     it('Should send a create password email', async function () {
       this.timeout(10000)
 
-      await createUser({
-        url: server.url,
-        accessToken: server.accessToken,
-        username: 'create_password',
-        password: ''
-      })
+      await server.users.create({ username: 'create_password', password: '' })
 
       await waitJobs(server)
       expect(emails).to.have.lengthOf(2)
@@ -170,15 +155,24 @@ describe('Test emails', function () {
     })
 
     it('Should not reset the password with an invalid verification string', async function () {
-      await resetPassword(server.url, userId2, verificationString2 + 'c', 'newly_created_password', HttpStatusCode.FORBIDDEN_403)
+      await server.users.resetPassword({
+        userId: userId2,
+        verificationString: verificationString2 + 'c',
+        password: 'newly_created_password',
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
     })
 
     it('Should reset the password', async function () {
-      await resetPassword(server.url, userId2, verificationString2, 'newly_created_password')
+      await server.users.resetPassword({
+        userId: userId2,
+        verificationString: verificationString2,
+        password: 'newly_created_password'
+      })
     })
 
     it('Should login with this new password', async function () {
-      await userLogin(server, {
+      await server.login.getAccessToken({
         username: 'create_password',
         password: 'newly_created_password'
       })
@@ -186,11 +180,12 @@ describe('Test emails', function () {
   })
 
   describe('When creating an abuse', function () {
+
     it('Should send the notification email', async function () {
       this.timeout(10000)
 
       const reason = 'my super bad reason'
-      await reportAbuse({ url: server.url, token: server.accessToken, videoId, reason })
+      await server.abuses.report({ videoId, reason })
 
       await waitJobs(server)
       expect(emails).to.have.lengthOf(3)
@@ -201,7 +196,7 @@ describe('Test emails', function () {
       expect(email['from'][0]['address']).equal('test-admin@localhost')
       expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com')
       expect(email['subject']).contains('abuse')
-      expect(email['text']).contains(videoUUID)
+      expect(email['text']).contains(videoShortUUID)
     })
   })
 
@@ -211,7 +206,7 @@ describe('Test emails', function () {
       this.timeout(10000)
 
       const reason = 'my super bad reason'
-      await blockUser(server.url, userId, server.accessToken, HttpStatusCode.NO_CONTENT_204, reason)
+      await server.users.banUser({ userId, reason })
 
       await waitJobs(server)
       expect(emails).to.have.lengthOf(4)
@@ -229,7 +224,7 @@ describe('Test emails', function () {
     it('Should send the notification email when unblocking a user', async function () {
       this.timeout(10000)
 
-      await unblockUser(server.url, userId, server.accessToken, HttpStatusCode.NO_CONTENT_204)
+      await server.users.unbanUser({ userId })
 
       await waitJobs(server)
       expect(emails).to.have.lengthOf(5)
@@ -249,7 +244,7 @@ describe('Test emails', function () {
       this.timeout(10000)
 
       const reason = 'my super reason'
-      await addVideoToBlacklist(server.url, server.accessToken, videoUserUUID, reason)
+      await server.blacklist.add({ videoId: videoUserUUID, reason })
 
       await waitJobs(server)
       expect(emails).to.have.lengthOf(6)
@@ -267,7 +262,7 @@ describe('Test emails', function () {
     it('Should send the notification email', async function () {
       this.timeout(10000)
 
-      await removeVideoFromBlacklist(server.url, server.accessToken, videoUserUUID)
+      await server.blacklist.remove({ videoId: videoUserUUID })
 
       await waitJobs(server)
       expect(emails).to.have.lengthOf(7)
@@ -292,7 +287,7 @@ describe('Test emails', function () {
     it('Should ask to send the verification email', async function () {
       this.timeout(10000)
 
-      await askSendVerifyEmail(server.url, 'user_1@example.com')
+      await server.users.askSendVerifyEmail({ email: 'user_1@example.com' })
 
       await waitJobs(server)
       expect(emails).to.have.lengthOf(8)
@@ -318,11 +313,16 @@ describe('Test emails', function () {
     })
 
     it('Should not verify the email with an invalid verification string', async function () {
-      await verifyEmail(server.url, userId, verificationString + 'b', false, HttpStatusCode.FORBIDDEN_403)
+      await server.users.verifyEmail({
+        userId,
+        verificationString: verificationString + 'b',
+        isPendingEmail: false,
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
     })
 
     it('Should verify the email', async function () {
-      await verifyEmail(server.url, userId, verificationString)
+      await server.users.verifyEmail({ userId, verificationString })
     })
   })
 
index 3f2f71f46762a32b54635025ef5aa7b10d9daac6..471f5d8d0891efacb49f2d621f299d4dabbbfd5d 100644 (file)
@@ -1,56 +1,41 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import {
-  cleanupTests,
-  doubleFollow,
-  flushAndRunMultipleServers,
-  getAccountVideos,
-  getVideo,
-  getVideoChannelVideos,
-  getVideoWithToken,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo
-} from '../../../../shared/extra-utils'
-import { unfollow } from '../../../../shared/extra-utils/server/follows'
-import { userLogin } from '../../../../shared/extra-utils/users/login'
-import { createUser } from '../../../../shared/extra-utils/users/users'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models'
+import * as chai from 'chai'
+import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers } from '@shared/extra-utils'
+import { HttpStatusCode, PeerTubeProblemDocument, ServerErrorCode } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test follow constraints', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let video1UUID: string
   let video2UUID: string
-  let userAccessToken: string
+  let userToken: string
 
   before(async function () {
     this.timeout(90000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
 
     {
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video server 1' })
-      video1UUID = res.body.video.uuid
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video server 1' } })
+      video1UUID = uuid
     }
     {
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video server 2' })
-      video2UUID = res.body.video.uuid
+      const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video server 2' } })
+      video2UUID = uuid
     }
 
     const user = {
       username: 'user1',
       password: 'super_password'
     }
-    await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
-    userAccessToken = await userLogin(servers[0], user)
+    await servers[0].users.create({ username: user.username, password: user.password })
+    userToken = await servers[0].login.getAccessToken(user)
 
     await doubleFollow(servers[0], servers[1])
   })
@@ -60,81 +45,81 @@ describe('Test follow constraints', function () {
     describe('With an unlogged user', function () {
 
       it('Should get the local video', async function () {
-        await getVideo(servers[0].url, video1UUID, HttpStatusCode.OK_200)
+        await servers[0].videos.get({ id: video1UUID })
       })
 
       it('Should get the remote video', async function () {
-        await getVideo(servers[0].url, video2UUID, HttpStatusCode.OK_200)
+        await servers[0].videos.get({ id: video2UUID })
       })
 
       it('Should list local account videos', async function () {
-        const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:' + servers[0].port, 0, 5)
+        const { total, data } = await servers[0].videos.listByAccount({ handle: 'root@localhost:' + servers[0].port })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should list remote account videos', async function () {
-        const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:' + servers[1].port, 0, 5)
+        const { total, data } = await servers[0].videos.listByAccount({ handle: 'root@localhost:' + servers[1].port })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should list local channel videos', async function () {
-        const videoChannelName = 'root_channel@localhost:' + servers[0].port
-        const res = await getVideoChannelVideos(servers[0].url, undefined, videoChannelName, 0, 5)
+        const handle = 'root_channel@localhost:' + servers[0].port
+        const { total, data } = await servers[0].videos.listByChannel({ handle })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should list remote channel videos', async function () {
-        const videoChannelName = 'root_channel@localhost:' + servers[1].port
-        const res = await getVideoChannelVideos(servers[0].url, undefined, videoChannelName, 0, 5)
+        const handle = 'root_channel@localhost:' + servers[1].port
+        const { total, data } = await servers[0].videos.listByChannel({ handle })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
     })
 
     describe('With a logged user', function () {
       it('Should get the local video', async function () {
-        await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, HttpStatusCode.OK_200)
+        await servers[0].videos.getWithToken({ token: userToken, id: video1UUID })
       })
 
       it('Should get the remote video', async function () {
-        await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, HttpStatusCode.OK_200)
+        await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
       })
 
       it('Should list local account videos', async function () {
-        const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:' + servers[0].port, 0, 5)
+        const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@localhost:' + servers[0].port })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should list remote account videos', async function () {
-        const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:' + servers[1].port, 0, 5)
+        const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@localhost:' + servers[1].port })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should list local channel videos', async function () {
-        const videoChannelName = 'root_channel@localhost:' + servers[0].port
-        const res = await getVideoChannelVideos(servers[0].url, userAccessToken, videoChannelName, 0, 5)
+        const handle = 'root_channel@localhost:' + servers[0].port
+        const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should list remote channel videos', async function () {
-        const videoChannelName = 'root_channel@localhost:' + servers[1].port
-        const res = await getVideoChannelVideos(servers[0].url, userAccessToken, videoChannelName, 0, 5)
+        const handle = 'root_channel@localhost:' + servers[1].port
+        const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
     })
   })
@@ -144,19 +129,18 @@ describe('Test follow constraints', function () {
     before(async function () {
       this.timeout(30000)
 
-      await unfollow(servers[0].url, servers[0].accessToken, servers[1])
+      await servers[0].follows.unfollow({ target: servers[1] })
     })
 
     describe('With an unlogged user', function () {
 
       it('Should get the local video', async function () {
-        await getVideo(servers[0].url, video1UUID, HttpStatusCode.OK_200)
+        await servers[0].videos.get({ id: video1UUID })
       })
 
       it('Should not get the remote video', async function () {
-        const res = await getVideo(servers[0].url, video2UUID, HttpStatusCode.FORBIDDEN_403)
-
-        const error = res.body as PeerTubeProblemDocument
+        const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
+        const error = body as unknown as PeerTubeProblemDocument
 
         const doc = 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/does_not_respect_follow_constraints'
         expect(error.type).to.equal(doc)
@@ -171,73 +155,79 @@ describe('Test follow constraints', function () {
       })
 
       it('Should list local account videos', async function () {
-        const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:' + servers[0].port, 0, 5)
+        const { total, data } = await servers[0].videos.listByAccount({
+          token: null,
+          handle: 'root@localhost:' + servers[0].port
+        })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should not list remote account videos', async function () {
-        const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:' + servers[1].port, 0, 5)
+        const { total, data } = await servers[0].videos.listByAccount({
+          token: null,
+          handle: 'root@localhost:' + servers[1].port
+        })
 
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
+        expect(total).to.equal(0)
+        expect(data).to.have.lengthOf(0)
       })
 
       it('Should list local channel videos', async function () {
-        const videoChannelName = 'root_channel@localhost:' + servers[0].port
-        const res = await getVideoChannelVideos(servers[0].url, undefined, videoChannelName, 0, 5)
+        const handle = 'root_channel@localhost:' + servers[0].port
+        const { total, data } = await servers[0].videos.listByChannel({ token: null, handle })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should not list remote channel videos', async function () {
-        const videoChannelName = 'root_channel@localhost:' + servers[1].port
-        const res = await getVideoChannelVideos(servers[0].url, undefined, videoChannelName, 0, 5)
+        const handle = 'root_channel@localhost:' + servers[1].port
+        const { total, data } = await servers[0].videos.listByChannel({ token: null, handle })
 
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
+        expect(total).to.equal(0)
+        expect(data).to.have.lengthOf(0)
       })
     })
 
     describe('With a logged user', function () {
       it('Should get the local video', async function () {
-        await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, HttpStatusCode.OK_200)
+        await servers[0].videos.getWithToken({ token: userToken, id: video1UUID })
       })
 
       it('Should get the remote video', async function () {
-        await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, HttpStatusCode.OK_200)
+        await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
       })
 
       it('Should list local account videos', async function () {
-        const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:' + servers[0].port, 0, 5)
+        const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@localhost:' + servers[0].port })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should list remote account videos', async function () {
-        const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:' + servers[1].port, 0, 5)
+        const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@localhost:' + servers[1].port })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should list local channel videos', async function () {
-        const videoChannelName = 'root_channel@localhost:' + servers[0].port
-        const res = await getVideoChannelVideos(servers[0].url, userAccessToken, videoChannelName, 0, 5)
+        const handle = 'root_channel@localhost:' + servers[0].port
+        const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
 
       it('Should list remote channel videos', async function () {
-        const videoChannelName = 'root_channel@localhost:' + servers[1].port
-        const res = await getVideoChannelVideos(servers[0].url, userAccessToken, videoChannelName, 0, 5)
+        const handle = 'root_channel@localhost:' + servers[1].port
+        const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       })
     })
   })
index 73c212a32333771236602488d6e02b669786c032..921f510435cf3131e6bd138b4481c79f24660ce9 100644 (file)
@@ -1,77 +1,66 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import {
-  acceptFollower,
   cleanupTests,
-  flushAndRunMultipleServers,
-  ServerInfo,
+  createMultipleServers,
+  FollowsCommand,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updateCustomSubConfig
-} from '../../../../shared/extra-utils/index'
-import {
-  follow,
-  getFollowersListPaginationAndSort,
-  getFollowingListPaginationAndSort,
-  rejectFollower,
-  removeFollower
-} from '../../../../shared/extra-utils/server/follows'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { ActorFollow } from '../../../../shared/models/actors'
+  waitJobs
+} from '@shared/extra-utils'
 
 const expect = chai.expect
 
-async function checkServer1And2HasFollowers (servers: ServerInfo[], state = 'accepted') {
-  {
-    const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: 'createdAt' })
-    expect(res.body.total).to.equal(1)
-
-    const follow = res.body.data[0] as ActorFollow
-    expect(follow.state).to.equal(state)
-    expect(follow.follower.url).to.equal('http://localhost:' + servers[0].port + '/accounts/peertube')
-    expect(follow.following.url).to.equal('http://localhost:' + servers[1].port + '/accounts/peertube')
-  }
+async function checkServer1And2HasFollowers (servers: PeerTubeServer[], state = 'accepted') {
+  const fns = [
+    servers[0].follows.getFollowings.bind(servers[0].follows),
+    servers[1].follows.getFollowers.bind(servers[1].follows)
+  ]
 
-  {
-    const res = await getFollowersListPaginationAndSort({ url: servers[1].url, start: 0, count: 5, sort: 'createdAt' })
-    expect(res.body.total).to.equal(1)
+  for (const fn of fns) {
+    const body = await fn({ start: 0, count: 5, sort: 'createdAt' })
+    expect(body.total).to.equal(1)
 
-    const follow = res.body.data[0] as ActorFollow
+    const follow = body.data[0]
     expect(follow.state).to.equal(state)
     expect(follow.follower.url).to.equal('http://localhost:' + servers[0].port + '/accounts/peertube')
     expect(follow.following.url).to.equal('http://localhost:' + servers[1].port + '/accounts/peertube')
   }
 }
 
-async function checkNoFollowers (servers: ServerInfo[]) {
-  {
-    const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: 'createdAt' })
-    expect(res.body.total).to.equal(0)
-  }
+async function checkNoFollowers (servers: PeerTubeServer[]) {
+  const fns = [
+    servers[0].follows.getFollowings.bind(servers[0].follows),
+    servers[1].follows.getFollowers.bind(servers[1].follows)
+  ]
 
-  {
-    const res = await getFollowersListPaginationAndSort({ url: servers[1].url, start: 0, count: 5, sort: 'createdAt' })
-    expect(res.body.total).to.equal(0)
+  for (const fn of fns) {
+    const body = await fn({ start: 0, count: 5, sort: 'createdAt' })
+    expect(body.total).to.equal(0)
   }
 }
 
 describe('Test follows moderation', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
+  let commands: FollowsCommand[]
 
   before(async function () {
     this.timeout(30000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
+
+    commands = servers.map(s => s.follows)
   })
 
   it('Should have server 1 following server 2', async function () {
     this.timeout(30000)
 
-    await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
+    await commands[0].follow({ hosts: [ servers[1].url ] })
 
     await waitJobs(servers)
   })
@@ -83,7 +72,7 @@ describe('Test follows moderation', function () {
   it('Should remove follower on server 2', async function () {
     this.timeout(10000)
 
-    await removeFollower(servers[1].url, servers[1].accessToken, servers[0])
+    await commands[1].removeFollower({ follower: servers[0] })
 
     await waitJobs(servers)
   })
@@ -104,9 +93,9 @@ describe('Test follows moderation', function () {
       }
     }
 
-    await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
+    await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
 
-    await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
+    await commands[0].follow({ hosts: [ servers[1].url ] })
     await waitJobs(servers)
 
     await checkNoFollowers(servers)
@@ -124,9 +113,9 @@ describe('Test follows moderation', function () {
       }
     }
 
-    await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
+    await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
 
-    await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
+    await commands[0].follow({ hosts: [ servers[1].url ] })
     await waitJobs(servers)
 
     await checkServer1And2HasFollowers(servers)
@@ -135,7 +124,7 @@ describe('Test follows moderation', function () {
   it('Should manually approve followers', async function () {
     this.timeout(20000)
 
-    await removeFollower(servers[1].url, servers[1].accessToken, servers[0])
+    await commands[1].removeFollower({ follower: servers[0] })
     await waitJobs(servers)
 
     const subConfig = {
@@ -147,10 +136,10 @@ describe('Test follows moderation', function () {
       }
     }
 
-    await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
-    await updateCustomSubConfig(servers[2].url, servers[2].accessToken, subConfig)
+    await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
+    await servers[2].config.updateCustomSubConfig({ newConfig: subConfig })
 
-    await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
+    await commands[0].follow({ hosts: [ servers[1].url ] })
     await waitJobs(servers)
 
     await checkServer1And2HasFollowers(servers, 'pending')
@@ -159,7 +148,7 @@ describe('Test follows moderation', function () {
   it('Should accept a follower', async function () {
     this.timeout(10000)
 
-    await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@localhost:' + servers[0].port)
+    await commands[1].acceptFollower({ follower: 'peertube@localhost:' + servers[0].port })
     await waitJobs(servers)
 
     await checkServer1And2HasFollowers(servers)
@@ -168,32 +157,32 @@ describe('Test follows moderation', function () {
   it('Should reject another follower', async function () {
     this.timeout(20000)
 
-    await follow(servers[0].url, [ servers[2].url ], servers[0].accessToken)
+    await commands[0].follow({ hosts: [ servers[2].url ] })
     await waitJobs(servers)
 
     {
-      const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: 'createdAt' })
-      expect(res.body.total).to.equal(2)
+      const body = await commands[0].getFollowings({ start: 0, count: 5, sort: 'createdAt' })
+      expect(body.total).to.equal(2)
     }
 
     {
-      const res = await getFollowersListPaginationAndSort({ url: servers[1].url, start: 0, count: 5, sort: 'createdAt' })
-      expect(res.body.total).to.equal(1)
+      const body = await commands[1].getFollowers({ start: 0, count: 5, sort: 'createdAt' })
+      expect(body.total).to.equal(1)
     }
 
     {
-      const res = await getFollowersListPaginationAndSort({ url: servers[2].url, start: 0, count: 5, sort: 'createdAt' })
-      expect(res.body.total).to.equal(1)
+      const body = await commands[2].getFollowers({ start: 0, count: 5, sort: 'createdAt' })
+      expect(body.total).to.equal(1)
     }
 
-    await rejectFollower(servers[2].url, servers[2].accessToken, 'peertube@localhost:' + servers[0].port)
+    await commands[2].rejectFollower({ follower: 'peertube@localhost:' + servers[0].port })
     await waitJobs(servers)
 
     await checkServer1And2HasFollowers(servers)
 
     {
-      const res = await getFollowersListPaginationAndSort({ url: servers[2].url, start: 0, count: 5, sort: 'createdAt' })
-      expect(res.body.total).to.equal(0)
+      const body = await commands[2].getFollowers({ start: 0, count: 5, sort: 'createdAt' })
+      expect(body.total).to.equal(0)
     }
   })
 
index 9e5aa00c75f1323129991f28c65efb13ce0f6225..832ba561ade5ed963a97d2d3dcd6f94a89a5e650 100644 (file)
 import 'mocha'
 import * as chai from 'chai'
 import {
-  addVideoCommentReply,
-  addVideoCommentThread,
   cleanupTests,
   completeVideoCheck,
-  createUser,
-  createVideoCaption,
+  createMultipleServers,
   dateIsValid,
-  deleteVideoComment,
   expectAccountFollows,
-  flushAndRunMultipleServers,
-  follow,
-  getFollowersListPaginationAndSort,
-  getFollowingListPaginationAndSort,
-  getVideoCommentThreads,
-  getVideosList,
-  getVideoThreadComments,
-  listVideoCaptions,
-  rateVideo,
-  ServerInfo,
+  expectChannelsFollows,
+  PeerTubeServer,
   setAccessTokensToServers,
   testCaptionFile,
-  unfollow,
-  uploadVideo,
-  userLogin,
   waitJobs
 } from '@shared/extra-utils'
-import { Video, VideoCaption, VideoComment, VideoCommentThreadTree, VideoPrivacy } from '@shared/models'
+import { VideoCreateResult, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test follows', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
 
   before(async function () {
     this.timeout(30000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
   })
 
-  it('Should not have followers', async function () {
-    for (const server of servers) {
-      const res = await getFollowersListPaginationAndSort({ url: server.url, start: 0, count: 5, sort: 'createdAt' })
-      const follows = res.body.data
+  describe('Data propagation after follow', function () {
 
-      expect(res.body.total).to.equal(0)
-      expect(follows).to.be.an('array')
-      expect(follows.length).to.equal(0)
-    }
-  })
-
-  it('Should not have following', async function () {
-    for (const server of servers) {
-      const res = await getFollowingListPaginationAndSort({ url: server.url, start: 0, count: 5, sort: 'createdAt' })
-      const follows = res.body.data
+    it('Should not have followers/followings', async function () {
+      for (const server of servers) {
+        const bodies = await Promise.all([
+          server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }),
+          server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
+        ])
 
-      expect(res.body.total).to.equal(0)
-      expect(follows).to.be.an('array')
-      expect(follows.length).to.equal(0)
-    }
-  })
+        for (const body of bodies) {
+          expect(body.total).to.equal(0)
 
-  it('Should have server 1 following server 2 and 3', async function () {
-    this.timeout(30000)
+          const follows = body.data
+          expect(follows).to.be.an('array')
+          expect(follows).to.have.lengthOf(0)
+        }
+      }
+    })
 
-    await follow(servers[0].url, [ servers[1].url, servers[2].url ], servers[0].accessToken)
+    it('Should have server 1 following root account of server 2 and server 3', async function () {
+      this.timeout(30000)
 
-    await waitJobs(servers)
-  })
+      await servers[0].follows.follow({
+        hosts: [ servers[2].url ],
+        handles: [ 'root@' + servers[1].host ]
+      })
 
-  it('Should have 2 followings on server 1', async function () {
-    let res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 1, sort: 'createdAt' })
-    let follows = res.body.data
+      await waitJobs(servers)
+    })
 
-    expect(res.body.total).to.equal(2)
-    expect(follows).to.be.an('array')
-    expect(follows.length).to.equal(1)
+    it('Should have 2 followings on server 1', async function () {
+      const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' })
+      expect(body.total).to.equal(2)
 
-    res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 1, count: 1, sort: 'createdAt' })
-    follows = follows.concat(res.body.data)
+      let follows = body.data
+      expect(follows).to.be.an('array')
+      expect(follows).to.have.lengthOf(1)
 
-    const server2Follow = follows.find(f => f.following.host === 'localhost:' + servers[1].port)
-    const server3Follow = follows.find(f => f.following.host === 'localhost:' + servers[2].port)
+      const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' })
+      follows = follows.concat(body2.data)
 
-    expect(server2Follow).to.not.be.undefined
-    expect(server3Follow).to.not.be.undefined
-    expect(server2Follow.state).to.equal('accepted')
-    expect(server3Follow.state).to.equal('accepted')
-  })
+      const server2Follow = follows.find(f => f.following.host === servers[1].host)
+      const server3Follow = follows.find(f => f.following.host === servers[2].host)
 
-  it('Should search/filter followings on server 1', async function () {
-    const sort = 'createdAt'
-    const start = 0
-    const count = 1
-    const url = servers[0].url
+      expect(server2Follow).to.not.be.undefined
+      expect(server2Follow.following.name).to.equal('root')
+      expect(server2Follow.state).to.equal('accepted')
 
-    {
-      const search = ':' + servers[1].port
+      expect(server3Follow).to.not.be.undefined
+      expect(server3Follow.following.name).to.equal('peertube')
+      expect(server3Follow.state).to.equal('accepted')
+    })
 
-      {
-        const res = await getFollowingListPaginationAndSort({ url, start, count, sort, search })
-        const follows = res.body.data
+    it('Should have 0 followings on server 2 and 3', async function () {
+      for (const server of [ servers[1], servers[2] ]) {
+        const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
+        expect(body.total).to.equal(0)
 
-        expect(res.body.total).to.equal(1)
-        expect(follows.length).to.equal(1)
-        expect(follows[0].following.host).to.equal('localhost:' + servers[1].port)
+        const follows = body.data
+        expect(follows).to.be.an('array')
+        expect(follows).to.have.lengthOf(0)
       }
+    })
 
-      {
-        const res = await getFollowingListPaginationAndSort({ url, start, count, sort, search, state: 'accepted' })
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+    it('Should have 1 followers on server 3', async function () {
+      const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
+      expect(body.total).to.equal(1)
+
+      const follows = body.data
+      expect(follows).to.be.an('array')
+      expect(follows).to.have.lengthOf(1)
+      expect(follows[0].follower.host).to.equal('localhost:' + servers[0].port)
+    })
+
+    it('Should have 0 followers on server 1 and 2', async function () {
+      for (const server of [ servers[0], servers[1] ]) {
+        const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' })
+        expect(body.total).to.equal(0)
+
+        const follows = body.data
+        expect(follows).to.be.an('array')
+        expect(follows).to.have.lengthOf(0)
       }
+    })
+
+    it('Should search/filter followings on server 1', async function () {
+      const sort = 'createdAt'
+      const start = 0
+      const count = 1
 
       {
-        const res = await getFollowingListPaginationAndSort({ url, start, count, sort, search, state: 'accepted', actorType: 'Person' })
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
+        const search = ':' + servers[1].port
+
+        {
+          const body = await servers[0].follows.getFollowings({ start, count, sort, search })
+          expect(body.total).to.equal(1)
+
+          const follows = body.data
+          expect(follows).to.have.lengthOf(1)
+          expect(follows[0].following.host).to.equal(servers[1].host)
+        }
+
+        {
+          const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' })
+          expect(body.total).to.equal(1)
+          expect(body.data).to.have.lengthOf(1)
+        }
+
+        {
+          const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
+          expect(body.total).to.equal(1)
+          expect(body.data).to.have.lengthOf(1)
+        }
+
+        {
+          const body = await servers[0].follows.getFollowings({
+            start,
+            count,
+            sort,
+            search,
+            state: 'accepted',
+            actorType: 'Application'
+          })
+          expect(body.total).to.equal(0)
+          expect(body.data).to.have.lengthOf(0)
+        }
+
+        {
+          const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' })
+          expect(body.total).to.equal(0)
+          expect(body.data).to.have.lengthOf(0)
+        }
       }
 
       {
-        const res = await getFollowingListPaginationAndSort({
-          url,
-          start,
-          count,
-          sort,
-          search,
-          state: 'accepted',
-          actorType: 'Application'
-        })
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' })
+        expect(body.total).to.equal(1)
+        expect(body.data).to.have.lengthOf(1)
       }
 
       {
-        const res = await getFollowingListPaginationAndSort({ url, start, count, sort, search, state: 'pending' })
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
+        const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' })
+        expect(body.total).to.equal(0)
+
+        expect(body.data).to.have.lengthOf(0)
       }
-    }
+    })
 
-    {
-      const res = await getFollowingListPaginationAndSort({ url, start, count, sort, search: 'bla' })
-      const follows = res.body.data
+    it('Should search/filter followers on server 2', async function () {
+      const start = 0
+      const count = 5
+      const sort = 'createdAt'
 
-      expect(res.body.total).to.equal(0)
-      expect(follows.length).to.equal(0)
-    }
-  })
+      {
+        const search = servers[0].port + ''
 
-  it('Should have 0 followings on server 2 and 3', async function () {
-    for (const server of [ servers[1], servers[2] ]) {
-      const res = await getFollowingListPaginationAndSort({ url: server.url, start: 0, count: 5, sort: 'createdAt' })
-      const follows = res.body.data
+        {
+          const body = await servers[2].follows.getFollowers({ start, count, sort, search })
+          expect(body.total).to.equal(1)
 
-      expect(res.body.total).to.equal(0)
-      expect(follows).to.be.an('array')
-      expect(follows.length).to.equal(0)
-    }
-  })
+          const follows = body.data
+          expect(follows).to.have.lengthOf(1)
+          expect(follows[0].following.host).to.equal(servers[2].host)
+        }
 
-  it('Should have 1 followers on server 2 and 3', async function () {
-    for (const server of [ servers[1], servers[2] ]) {
-      const res = await getFollowersListPaginationAndSort({ url: server.url, start: 0, count: 1, sort: 'createdAt' })
+        {
+          const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' })
+          expect(body.total).to.equal(1)
+          expect(body.data).to.have.lengthOf(1)
+        }
 
-      const follows = res.body.data
-      expect(res.body.total).to.equal(1)
-      expect(follows).to.be.an('array')
-      expect(follows.length).to.equal(1)
-      expect(follows[0].follower.host).to.equal('localhost:' + servers[0].port)
-    }
-  })
+        {
+          const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
+          expect(body.total).to.equal(0)
+          expect(body.data).to.have.lengthOf(0)
+        }
 
-  it('Should search/filter followers on server 2', async function () {
-    const url = servers[2].url
-    const start = 0
-    const count = 5
-    const sort = 'createdAt'
+        {
+          const body = await servers[2].follows.getFollowers({
+            start,
+            count,
+            sort,
+            search,
+            state: 'accepted',
+            actorType: 'Application'
+          })
+          expect(body.total).to.equal(1)
+          expect(body.data).to.have.lengthOf(1)
+        }
 
-    {
-      const search = servers[0].port + ''
+        {
+          const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' })
+          expect(body.total).to.equal(0)
+          expect(body.data).to.have.lengthOf(0)
+        }
+      }
 
       {
-        const res = await getFollowersListPaginationAndSort({ url, start, count, sort, search })
-        const follows = res.body.data
+        const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' })
+        expect(body.total).to.equal(0)
 
-        expect(res.body.total).to.equal(1)
-        expect(follows.length).to.equal(1)
-        expect(follows[0].following.host).to.equal('localhost:' + servers[2].port)
+        const follows = body.data
+        expect(follows).to.have.lengthOf(0)
       }
+    })
 
-      {
-        const res = await getFollowersListPaginationAndSort({ url, start, count, sort, search, state: 'accepted' })
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
-      }
+    it('Should have the correct follows counts', async function () {
+      await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 })
+      await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
+      await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
 
-      {
-        const res = await getFollowersListPaginationAndSort({ url, start, count, sort, search, state: 'accepted', actorType: 'Person' })
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
-      }
+      // Server 2 and 3 does not know server 1 follow another server (there was not a refresh)
+      await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+      await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
+      await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
 
-      {
-        const res = await getFollowersListPaginationAndSort({
-          url,
-          start,
-          count,
-          sort,
-          search,
-          state: 'accepted',
-          actorType: 'Application'
-        })
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
-      }
+      await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+      await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
+    })
 
-      {
-        const res = await getFollowersListPaginationAndSort({ url, start, count, sort, search, state: 'pending' })
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
-      }
-    }
+    it('Should unfollow server 3 on server 1', async function () {
+      this.timeout(15000)
 
-    {
-      const res = await getFollowersListPaginationAndSort({ url, start, count, sort, search: 'bla' })
-      const follows = res.body.data
+      await servers[0].follows.unfollow({ target: servers[2] })
 
-      expect(res.body.total).to.equal(0)
-      expect(follows.length).to.equal(0)
-    }
-  })
+      await waitJobs(servers)
+    })
 
-  it('Should have 0 followers on server 1', async function () {
-    const res = await getFollowersListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: 'createdAt' })
-    const follows = res.body.data
+    it('Should not follow server 3 on server 1 anymore', async function () {
+      const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' })
+      expect(body.total).to.equal(1)
 
-    expect(res.body.total).to.equal(0)
-    expect(follows).to.be.an('array')
-    expect(follows.length).to.equal(0)
-  })
+      const follows = body.data
+      expect(follows).to.be.an('array')
+      expect(follows).to.have.lengthOf(1)
 
-  it('Should have the correct follows counts', async function () {
-    await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[0].port, 0, 2)
-    await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[1].port, 1, 0)
-    await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[2].port, 1, 0)
+      expect(follows[0].following.host).to.equal(servers[1].host)
+    })
 
-    // Server 2 and 3 does not know server 1 follow another server (there was not a refresh)
-    await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[0].port, 0, 1)
-    await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[1].port, 1, 0)
+    it('Should not have server 1 as follower on server 3 anymore', async function () {
+      const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
+      expect(body.total).to.equal(0)
 
-    await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[0].port, 0, 1)
-    await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[2].port, 1, 0)
-  })
+      const follows = body.data
+      expect(follows).to.be.an('array')
+      expect(follows).to.have.lengthOf(0)
+    })
 
-  it('Should unfollow server 3 on server 1', async function () {
-    this.timeout(5000)
+    it('Should have the correct follows counts after the unfollow', async function () {
+      await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+      await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
+      await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
 
-    await unfollow(servers[0].url, servers[0].accessToken, servers[2])
+      await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+      await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
+      await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
 
-    await waitJobs(servers)
-  })
+      await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 })
+      await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
+    })
 
-  it('Should not follow server 3 on server 1 anymore', async function () {
-    const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 2, sort: 'createdAt' })
-    const follows = res.body.data
+    it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () {
+      this.timeout(60000)
 
-    expect(res.body.total).to.equal(1)
-    expect(follows).to.be.an('array')
-    expect(follows.length).to.equal(1)
+      await servers[1].videos.upload({ attributes: { name: 'server2' } })
+      await servers[2].videos.upload({ attributes: { name: 'server3' } })
 
-    expect(follows[0].following.host).to.equal('localhost:' + servers[1].port)
-  })
+      await waitJobs(servers)
 
-  it('Should not have server 1 as follower on server 3 anymore', async function () {
-    const res = await getFollowersListPaginationAndSort({ url: servers[2].url, start: 0, count: 1, sort: 'createdAt' })
+      {
+        const { total, data } = await servers[0].videos.list()
+        expect(total).to.equal(1)
+        expect(data[0].name).to.equal('server2')
+      }
 
-    const follows = res.body.data
-    expect(res.body.total).to.equal(0)
-    expect(follows).to.be.an('array')
-    expect(follows.length).to.equal(0)
-  })
+      {
+        const { total, data } = await servers[1].videos.list()
+        expect(total).to.equal(1)
+        expect(data[0].name).to.equal('server2')
+      }
+
+      {
+        const { total, data } = await servers[2].videos.list()
+        expect(total).to.equal(1)
+        expect(data[0].name).to.equal('server3')
+      }
+    })
 
-  it('Should have the correct follows counts 2', async function () {
-    await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[0].port, 0, 1)
-    await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[1].port, 1, 0)
+    it('Should remove account follow', async function () {
+      this.timeout(15000)
 
-    await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[0].port, 0, 1)
-    await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[1].port, 1, 0)
+      await servers[0].follows.unfollow({ target: 'root@' + servers[1].host })
 
-    await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[0].port, 0, 0)
-    await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[2].port, 0, 0)
-  })
+      await waitJobs(servers)
+    })
 
-  it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () {
-    this.timeout(60000)
+    it('Should have removed the account follow', async function () {
+      await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
+      await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
 
-    await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'server2' })
-    await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3' })
+      {
+        const { total, data } = await servers[0].follows.getFollowings()
+        expect(total).to.equal(0)
+        expect(data).to.have.lengthOf(0)
+      }
 
-    await waitJobs(servers)
+      {
+        const { total, data } = await servers[0].videos.list()
+        expect(total).to.equal(0)
+        expect(data).to.have.lengthOf(0)
+      }
+    })
 
-    let res = await getVideosList(servers[0].url)
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data[0].name).to.equal('server2')
+    it('Should follow a channel', async function () {
+      this.timeout(15000)
 
-    res = await getVideosList(servers[1].url)
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data[0].name).to.equal('server2')
+      await servers[0].follows.follow({
+        handles: [ 'root_channel@' + servers[1].host ]
+      })
 
-    res = await getVideosList(servers[2].url)
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data[0].name).to.equal('server3')
+      await waitJobs(servers)
+
+      await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
+      await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
+
+      {
+        const { total, data } = await servers[0].follows.getFollowings()
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
+      }
+
+      {
+        const { total, data } = await servers[0].videos.list()
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
+      }
+    })
   })
 
-  describe('Should propagate data on a new following', function () {
-    let video4: Video
+  describe('Should propagate data on a new server follow', function () {
+    let video4: VideoCreateResult
 
     before(async function () {
       this.timeout(50000)
@@ -334,100 +383,75 @@ describe('Test follows', function () {
         tags: [ 'tag1', 'tag2', 'tag3' ]
       }
 
-      await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-2' })
-      await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-3' })
-      await uploadVideo(servers[2].url, servers[2].accessToken, video4Attributes)
-      await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-5' })
-      await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-6' })
+      await servers[2].videos.upload({ attributes: { name: 'server3-2' } })
+      await servers[2].videos.upload({ attributes: { name: 'server3-3' } })
+      video4 = await servers[2].videos.upload({ attributes: video4Attributes })
+      await servers[2].videos.upload({ attributes: { name: 'server3-5' } })
+      await servers[2].videos.upload({ attributes: { name: 'server3-6' } })
 
       {
-        const user = { username: 'captain', password: 'password' }
-        await createUser({ url: servers[2].url, accessToken: servers[2].accessToken, username: user.username, password: user.password })
-        const userAccessToken = await userLogin(servers[2], user)
-
-        const resVideos = await getVideosList(servers[2].url)
-        video4 = resVideos.body.data.find(v => v.name === 'server3-4')
+        const userAccessToken = await servers[2].users.generateUserAndToken('captain')
 
-        {
-          await rateVideo(servers[2].url, servers[2].accessToken, video4.id, 'like')
-          await rateVideo(servers[2].url, userAccessToken, video4.id, 'dislike')
-        }
-
-        {
-          {
-            const text = 'my super first comment'
-            const res = await addVideoCommentThread(servers[2].url, servers[2].accessToken, video4.id, text)
-            const threadId = res.body.comment.id
-
-            const text1 = 'my super answer to thread 1'
-            const childCommentRes = await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text1)
-            const childCommentId = childCommentRes.body.comment.id
-
-            const text2 = 'my super answer to answer of thread 1'
-            await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, childCommentId, text2)
-
-            const text3 = 'my second answer to thread 1'
-            await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text3)
-          }
+        await servers[2].videos.rate({ id: video4.id, rating: 'like' })
+        await servers[2].videos.rate({ token: userAccessToken, id: video4.id, rating: 'dislike' })
+      }
 
-          {
-            const text = 'will be deleted'
-            const res = await addVideoCommentThread(servers[2].url, servers[2].accessToken, video4.id, text)
-            const threadId = res.body.comment.id
+      {
+        await servers[2].comments.createThread({ videoId: video4.id, text: 'my super first comment' })
 
-            const text1 = 'answer to deleted'
-            await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text1)
+        await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' })
+        await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' })
+        await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' })
+      }
 
-            const text2 = 'will also be deleted'
-            const childCommentRes = await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text2)
-            const childCommentId = childCommentRes.body.comment.id
+      {
+        const { id: threadId } = await servers[2].comments.createThread({ videoId: video4.id, text: 'will be deleted' })
+        await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' })
 
-            const text3 = 'my second answer to deleted'
-            await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, childCommentId, text3)
+        const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' })
 
-            await deleteVideoComment(servers[2].url, servers[2].accessToken, video4.id, threadId)
-            await deleteVideoComment(servers[2].url, servers[2].accessToken, video4.id, childCommentId)
-          }
-        }
+        await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' })
 
-        {
-          await createVideoCaption({
-            url: servers[2].url,
-            accessToken: servers[2].accessToken,
-            language: 'ar',
-            videoId: video4.id,
-            fixture: 'subtitle-good2.vtt'
-          })
-        }
+        await servers[2].comments.delete({ videoId: video4.id, commentId: threadId })
+        await servers[2].comments.delete({ videoId: video4.id, commentId: replyId })
       }
 
+      await servers[2].captions.add({
+        language: 'ar',
+        videoId: video4.id,
+        fixture: 'subtitle-good2.vtt'
+      })
+
       await waitJobs(servers)
 
       // Server 1 follows server 3
-      await follow(servers[0].url, [ servers[2].url ], servers[0].accessToken)
+      await servers[0].follows.follow({ hosts: [ servers[2].url ] })
 
       await waitJobs(servers)
     })
 
-    it('Should have the correct follows counts 3', async function () {
-      await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[0].port, 0, 2)
-      await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[1].port, 1, 0)
-      await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[2].port, 1, 0)
+    it('Should have the correct follows counts', async function () {
+      await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 })
+      await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
+      await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
+      await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
 
-      await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[0].port, 0, 1)
-      await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[1].port, 1, 0)
+      await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+      await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
+      await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
+      await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
 
-      await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[0].port, 0, 1)
-      await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[2].port, 1, 0)
+      await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+      await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
     })
 
     it('Should have propagated videos', async function () {
-      const res = await getVideosList(servers[0].url)
-      expect(res.body.total).to.equal(7)
+      const { total, data } = await servers[0].videos.list()
+      expect(total).to.equal(7)
 
-      const video2 = res.body.data.find(v => v.name === 'server3-2')
-      video4 = res.body.data.find(v => v.name === 'server3-4')
-      const video6 = res.body.data.find(v => v.name === 'server3-6')
+      const video2 = data.find(v => v.name === 'server3-2')
+      video4 = data.find(v => v.name === 'server3-4')
+      const video6 = data.find(v => v.name === 'server3-6')
 
       expect(video2).to.not.be.undefined
       expect(video4).to.not.be.undefined
@@ -444,7 +468,7 @@ describe('Test follows', function () {
         support: 'my super support text',
         account: {
           name: 'root',
-          host: 'localhost:' + servers[2].port
+          host: servers[2].host
         },
         isLocal,
         commentsEnabled: true,
@@ -468,33 +492,31 @@ describe('Test follows', function () {
           }
         ]
       }
-      await completeVideoCheck(servers[0].url, video4, checkAttributes)
+      await completeVideoCheck(servers[0], video4, checkAttributes)
     })
 
     it('Should have propagated comments', async function () {
-      const res1 = await getVideoCommentThreads(servers[0].url, video4.id, 0, 5, 'createdAt')
+      const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' })
 
-      expect(res1.body.total).to.equal(2)
-      expect(res1.body.data).to.be.an('array')
-      expect(res1.body.data).to.have.lengthOf(2)
+      expect(total).to.equal(2)
+      expect(data).to.be.an('array')
+      expect(data).to.have.lengthOf(2)
 
       {
-        const comment: VideoComment = res1.body.data[0]
+        const comment = data[0]
         expect(comment.inReplyToCommentId).to.be.null
         expect(comment.text).equal('my super first comment')
         expect(comment.videoId).to.equal(video4.id)
         expect(comment.id).to.equal(comment.threadId)
         expect(comment.account.name).to.equal('root')
-        expect(comment.account.host).to.equal('localhost:' + servers[2].port)
+        expect(comment.account.host).to.equal(servers[2].host)
         expect(comment.totalReplies).to.equal(3)
         expect(dateIsValid(comment.createdAt as string)).to.be.true
         expect(dateIsValid(comment.updatedAt as string)).to.be.true
 
         const threadId = comment.threadId
 
-        const res2 = await getVideoThreadComments(servers[0].url, video4.id, threadId)
-
-        const tree: VideoCommentThreadTree = res2.body
+        const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId })
         expect(tree.comment.text).equal('my super first comment')
         expect(tree.children).to.have.lengthOf(2)
 
@@ -512,7 +534,7 @@ describe('Test follows', function () {
       }
 
       {
-        const deletedComment: VideoComment = res1.body.data[1]
+        const deletedComment = data[1]
         expect(deletedComment).to.not.be.undefined
         expect(deletedComment.isDeleted).to.be.true
         expect(deletedComment.deletedAt).to.not.be.null
@@ -522,9 +544,7 @@ describe('Test follows', function () {
         expect(deletedComment.totalReplies).to.equal(2)
         expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
 
-        const res2 = await getVideoThreadComments(servers[0].url, video4.id, deletedComment.threadId)
-
-        const tree: VideoCommentThreadTree = res2.body
+        const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId })
         const [ commentRoot, deletedChildRoot ] = tree.children
 
         expect(deletedChildRoot).to.not.be.undefined
@@ -549,11 +569,11 @@ describe('Test follows', function () {
     })
 
     it('Should have propagated captions', async function () {
-      const res = await listVideoCaptions(servers[0].url, video4.id)
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
+      const body = await servers[0].captions.list({ videoId: video4.id })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
 
-      const caption1: VideoCaption = res.body.data[0]
+      const caption1 = body.data[0]
       expect(caption1.language.id).to.equal('ar')
       expect(caption1.language.label).to.equal('Arabic')
       expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$'))
@@ -563,14 +583,39 @@ describe('Test follows', function () {
     it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () {
       this.timeout(5000)
 
-      await unfollow(servers[0].url, servers[0].accessToken, servers[2])
+      await servers[0].follows.unfollow({ target: servers[2] })
+
+      await waitJobs(servers)
+
+      const { total } = await servers[0].videos.list()
+      expect(total).to.equal(1)
+    })
+  })
+
+  describe('Should propagate data on a new channel follow', function () {
+
+    before(async function () {
+      this.timeout(60000)
+
+      await servers[2].videos.upload({ attributes: { name: 'server3-7' } })
 
       await waitJobs(servers)
 
-      const res = await getVideosList(servers[0].url)
-      expect(res.body.total).to.equal(1)
+      const video = await servers[0].videos.find({ name: 'server3-7' })
+      expect(video).to.not.exist
     })
 
+    it('Should have propagated channel video', async function () {
+      this.timeout(60000)
+
+      await servers[0].follows.follow({ handles: [ 'root_channel@' + servers[2].host ] })
+
+      await waitJobs(servers)
+
+      const video = await servers[0].videos.find({ name: 'server3-7' })
+
+      expect(video).to.exist
+    })
   })
 
   after(async function () {
index d57d72f5e4763ee50516df698b1ef759f8286f0d..2f39503540b646e80690c4282c93baaeace08dfd 100644 (file)
@@ -1,51 +1,31 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { JobState, Video } from '../../../../shared/models'
-import { VideoPrivacy } from '../../../../shared/models/videos'
-import { VideoCommentThreadTree } from '../../../../shared/models/videos/comment/video-comment.model'
-
+import * as chai from 'chai'
 import {
   cleanupTests,
-  closeAllSequelize,
+  CommentsCommand,
   completeVideoCheck,
-  flushAndRunMultipleServers,
-  getVideo,
-  getVideosList,
-  immutableAssign,
+  createMultipleServers,
   killallServers,
-  reRunServer,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  setActorFollowScores,
-  unfollow,
-  updateVideo,
-  uploadVideo,
-  uploadVideoAndGetId,
-  wait
-} from '../../../../shared/extra-utils'
-import { follow, getFollowersListPaginationAndSort } from '../../../../shared/extra-utils/server/follows'
-import { getJobsListPaginationAndSort, waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import {
-  addVideoCommentReply,
-  addVideoCommentThread,
-  getVideoCommentThreads,
-  getVideoThreadComments
-} from '../../../../shared/extra-utils/videos/video-comments'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { HttpStatusCode, JobState, VideoCreateResult, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test handle downs', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let threadIdServer1: number
   let threadIdServer2: number
   let commentIdServer1: number
   let commentIdServer2: number
-  let missedVideo1: Video
-  let missedVideo2: Video
-  let unlistedVideo: Video
+  let missedVideo1: VideoCreateResult
+  let missedVideo2: VideoCreateResult
+  let unlistedVideo: VideoCreateResult
 
   const videoIdsServer1: string[] = []
 
@@ -62,17 +42,18 @@ describe('Test handle downs', function () {
     fixture: 'video_short1.webm'
   }
 
-  const unlistedVideoAttributes = immutableAssign(videoAttributes, {
-    privacy: VideoPrivacy.UNLISTED
-  })
+  const unlistedVideoAttributes = { ...videoAttributes, privacy: VideoPrivacy.UNLISTED }
 
   let checkAttributes: any
   let unlistedCheckAttributes: any
 
+  let commentCommands: CommentsCommand[]
+
   before(async function () {
     this.timeout(30000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
+    commentCommands = servers.map(s => s.comments)
 
     checkAttributes = {
       name: 'my super name for server 1',
@@ -106,9 +87,7 @@ describe('Test handle downs', function () {
         }
       ]
     }
-    unlistedCheckAttributes = immutableAssign(checkAttributes, {
-      privacy: VideoPrivacy.UNLISTED
-    })
+    unlistedCheckAttributes = { ...checkAttributes, privacy: VideoPrivacy.UNLISTED }
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -118,58 +97,53 @@ describe('Test handle downs', function () {
     this.timeout(240000)
 
     // Server 2 and 3 follow server 1
-    await follow(servers[1].url, [ servers[0].url ], servers[1].accessToken)
-    await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
+    await servers[1].follows.follow({ hosts: [ servers[0].url ] })
+    await servers[2].follows.follow({ hosts: [ servers[0].url ] })
 
     await waitJobs(servers)
 
     // Upload a video to server 1
-    await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
+    await servers[0].videos.upload({ attributes: videoAttributes })
 
     await waitJobs(servers)
 
     // And check all servers have this video
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
+      const { data } = await server.videos.list()
+      expect(data).to.be.an('array')
+      expect(data).to.have.lengthOf(1)
     }
 
     // Kill server 2
-    killallServers([ servers[1] ])
+    await killallServers([ servers[1] ])
 
     // Remove server 2 follower
     for (let i = 0; i < 10; i++) {
-      await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
+      await servers[0].videos.upload({ attributes: videoAttributes })
     }
 
     await waitJobs([ servers[0], servers[2] ])
 
     // Kill server 3
-    killallServers([ servers[2] ])
+    await killallServers([ servers[2] ])
 
-    const resLastVideo1 = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
-    missedVideo1 = resLastVideo1.body.video
+    missedVideo1 = await servers[0].videos.upload({ attributes: videoAttributes })
 
-    const resLastVideo2 = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
-    missedVideo2 = resLastVideo2.body.video
+    missedVideo2 = await servers[0].videos.upload({ attributes: videoAttributes })
 
     // Unlisted video
-    const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, unlistedVideoAttributes)
-    unlistedVideo = resVideo.body.video
+    unlistedVideo = await servers[0].videos.upload({ attributes: unlistedVideoAttributes })
 
     // Add comments to video 2
     {
       const text = 'thread 1'
-      let resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, missedVideo2.uuid, text)
-      let comment = resComment.body.comment
+      let comment = await commentCommands[0].createThread({ videoId: missedVideo2.uuid, text })
       threadIdServer1 = comment.id
 
-      resComment = await addVideoCommentReply(servers[0].url, servers[0].accessToken, missedVideo2.uuid, comment.id, 'comment 1-1')
-      comment = resComment.body.comment
+      comment = await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: comment.id, text: 'comment 1-1' })
 
-      resComment = await addVideoCommentReply(servers[0].url, servers[0].accessToken, missedVideo2.uuid, comment.id, 'comment 1-2')
-      commentIdServer1 = resComment.body.comment.id
+      const created = await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: comment.id, text: 'comment 1-2' })
+      commentIdServer1 = created.id
     }
 
     await waitJobs(servers[0])
@@ -177,90 +151,87 @@ describe('Test handle downs', function () {
     await wait(11000)
 
     // Only server 3 is still a follower of server 1
-    const res = await getFollowersListPaginationAndSort({ url: servers[0].url, start: 0, count: 2, sort: 'createdAt' })
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data).to.have.lengthOf(1)
-    expect(res.body.data[0].follower.host).to.equal('localhost:' + servers[2].port)
+    const body = await servers[0].follows.getFollowers({ start: 0, count: 2, sort: 'createdAt' })
+    expect(body.data).to.be.an('array')
+    expect(body.data).to.have.lengthOf(1)
+    expect(body.data[0].follower.host).to.equal('localhost:' + servers[2].port)
   })
 
   it('Should not have pending/processing jobs anymore', async function () {
     const states: JobState[] = [ 'waiting', 'active' ]
 
     for (const state of states) {
-      const res = await getJobsListPaginationAndSort({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
+      const body = await servers[0].jobs.getJobsList({
         state: state,
         start: 0,
         count: 50,
         sort: '-createdAt'
       })
-      expect(res.body.data).to.have.length(0)
+      expect(body.data).to.have.length(0)
     }
   })
 
   it('Should re-follow server 1', async function () {
     this.timeout(35000)
 
-    await reRunServer(servers[1])
-    await reRunServer(servers[2])
+    await servers[1].run()
+    await servers[2].run()
 
-    await unfollow(servers[1].url, servers[1].accessToken, servers[0])
+    await servers[1].follows.unfollow({ target: servers[0] })
     await waitJobs(servers)
 
-    await follow(servers[1].url, [ servers[0].url ], servers[1].accessToken)
+    await servers[1].follows.follow({ hosts: [ servers[0].url ] })
 
     await waitJobs(servers)
 
-    const res = await getFollowersListPaginationAndSort({ url: servers[0].url, start: 0, count: 2, sort: 'createdAt' })
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data).to.have.lengthOf(2)
+    const body = await servers[0].follows.getFollowers({ start: 0, count: 2, sort: 'createdAt' })
+    expect(body.data).to.be.an('array')
+    expect(body.data).to.have.lengthOf(2)
   })
 
   it('Should send an update to server 3, and automatically fetch the video', async function () {
     this.timeout(15000)
 
-    const res1 = await getVideosList(servers[2].url)
-    expect(res1.body.data).to.be.an('array')
-    expect(res1.body.data).to.have.lengthOf(11)
+    {
+      const { data } = await servers[2].videos.list()
+      expect(data).to.be.an('array')
+      expect(data).to.have.lengthOf(11)
+    }
 
-    await updateVideo(servers[0].url, servers[0].accessToken, missedVideo1.uuid, {})
-    await updateVideo(servers[0].url, servers[0].accessToken, unlistedVideo.uuid, {})
+    await servers[0].videos.update({ id: missedVideo1.uuid })
+    await servers[0].videos.update({ id: unlistedVideo.uuid })
 
     await waitJobs(servers)
 
-    const res = await getVideosList(servers[2].url)
-    expect(res.body.data).to.be.an('array')
-    // 1 video is unlisted
-    expect(res.body.data).to.have.lengthOf(12)
+    {
+      const { data } = await servers[2].videos.list()
+      expect(data).to.be.an('array')
+      // 1 video is unlisted
+      expect(data).to.have.lengthOf(12)
+    }
 
     // Check unlisted video
-    const resVideo = await getVideo(servers[2].url, unlistedVideo.uuid)
-    expect(resVideo.body).not.to.be.undefined
-
-    await completeVideoCheck(servers[2].url, resVideo.body, unlistedCheckAttributes)
+    const video = await servers[2].videos.get({ id: unlistedVideo.uuid })
+    await completeVideoCheck(servers[2], video, unlistedCheckAttributes)
   })
 
   it('Should send comments on a video to server 3, and automatically fetch the video', async function () {
     this.timeout(25000)
 
-    await addVideoCommentReply(servers[0].url, servers[0].accessToken, missedVideo2.uuid, commentIdServer1, 'comment 1-3')
+    await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: commentIdServer1, text: 'comment 1-3' })
 
     await waitJobs(servers)
 
-    const resVideo = await getVideo(servers[2].url, missedVideo2.uuid)
-    expect(resVideo.body).not.to.be.undefined
+    await servers[2].videos.get({ id: missedVideo2.uuid })
 
     {
-      let resComment = await getVideoCommentThreads(servers[2].url, missedVideo2.uuid, 0, 5)
-      expect(resComment.body.data).to.be.an('array')
-      expect(resComment.body.data).to.have.lengthOf(1)
-
-      threadIdServer2 = resComment.body.data[0].id
+      const { data } = await servers[2].comments.listThreads({ videoId: missedVideo2.uuid })
+      expect(data).to.be.an('array')
+      expect(data).to.have.lengthOf(1)
 
-      resComment = await getVideoThreadComments(servers[2].url, missedVideo2.uuid, threadIdServer2)
+      threadIdServer2 = data[0].id
 
-      const tree: VideoCommentThreadTree = resComment.body
+      const tree = await servers[2].comments.getThread({ videoId: missedVideo2.uuid, threadId: threadIdServer2 })
       expect(tree.comment.text).equal('thread 1')
       expect(tree.children).to.have.lengthOf(1)
 
@@ -283,57 +254,54 @@ describe('Test handle downs', function () {
   it('Should correctly reply to the comment', async function () {
     this.timeout(15000)
 
-    await addVideoCommentReply(servers[2].url, servers[2].accessToken, missedVideo2.uuid, commentIdServer2, 'comment 1-4')
+    await servers[2].comments.addReply({ videoId: missedVideo2.uuid, toCommentId: commentIdServer2, text: 'comment 1-4' })
 
     await waitJobs(servers)
 
-    {
-      const resComment = await getVideoThreadComments(servers[0].url, missedVideo2.uuid, threadIdServer1)
+    const tree = await commentCommands[0].getThread({ videoId: missedVideo2.uuid, threadId: threadIdServer1 })
 
-      const tree: VideoCommentThreadTree = resComment.body
-      expect(tree.comment.text).equal('thread 1')
-      expect(tree.children).to.have.lengthOf(1)
+    expect(tree.comment.text).equal('thread 1')
+    expect(tree.children).to.have.lengthOf(1)
 
-      const firstChild = tree.children[0]
-      expect(firstChild.comment.text).to.equal('comment 1-1')
-      expect(firstChild.children).to.have.lengthOf(1)
+    const firstChild = tree.children[0]
+    expect(firstChild.comment.text).to.equal('comment 1-1')
+    expect(firstChild.children).to.have.lengthOf(1)
 
-      const childOfFirstChild = firstChild.children[0]
-      expect(childOfFirstChild.comment.text).to.equal('comment 1-2')
-      expect(childOfFirstChild.children).to.have.lengthOf(1)
+    const childOfFirstChild = firstChild.children[0]
+    expect(childOfFirstChild.comment.text).to.equal('comment 1-2')
+    expect(childOfFirstChild.children).to.have.lengthOf(1)
 
-      const childOfChildFirstChild = childOfFirstChild.children[0]
-      expect(childOfChildFirstChild.comment.text).to.equal('comment 1-3')
-      expect(childOfChildFirstChild.children).to.have.lengthOf(1)
+    const childOfChildFirstChild = childOfFirstChild.children[0]
+    expect(childOfChildFirstChild.comment.text).to.equal('comment 1-3')
+    expect(childOfChildFirstChild.children).to.have.lengthOf(1)
 
-      const childOfChildOfChildOfFirstChild = childOfChildFirstChild.children[0]
-      expect(childOfChildOfChildOfFirstChild.comment.text).to.equal('comment 1-4')
-      expect(childOfChildOfChildOfFirstChild.children).to.have.lengthOf(0)
-    }
+    const childOfChildOfChildOfFirstChild = childOfChildFirstChild.children[0]
+    expect(childOfChildOfChildOfFirstChild.comment.text).to.equal('comment 1-4')
+    expect(childOfChildOfChildOfFirstChild.children).to.have.lengthOf(0)
   })
 
   it('Should upload many videos on server 1', async function () {
     this.timeout(120000)
 
     for (let i = 0; i < 10; i++) {
-      const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video ' + i })).uuid
+      const uuid = (await servers[0].videos.quickUpload({ name: 'video ' + i })).uuid
       videoIdsServer1.push(uuid)
     }
 
     await waitJobs(servers)
 
     for (const id of videoIdsServer1) {
-      await getVideo(servers[1].url, id)
+      await servers[1].videos.get({ id })
     }
 
     await waitJobs(servers)
-    await setActorFollowScores(servers[1].internalServerNumber, 20)
+    await servers[1].sql.setActorFollowScores(20)
 
     // Wait video expiration
     await wait(11000)
 
     // Refresh video -> score + 10 = 30
-    await getVideo(servers[1].url, videoIdsServer1[0])
+    await servers[1].videos.get({ id: videoIdsServer1[0] })
 
     await waitJobs(servers)
   })
@@ -341,27 +309,25 @@ describe('Test handle downs', function () {
   it('Should remove followings that are down', async function () {
     this.timeout(120000)
 
-    killallServers([ servers[0] ])
+    await killallServers([ servers[0] ])
 
     // Wait video expiration
     await wait(11000)
 
     for (let i = 0; i < 5; i++) {
       try {
-        await getVideo(servers[1].url, videoIdsServer1[i])
+        await servers[1].videos.get({ id: videoIdsServer1[i] })
         await waitJobs([ servers[1] ])
         await wait(1500)
       } catch {}
     }
 
     for (const id of videoIdsServer1) {
-      await getVideo(servers[1].url, id, HttpStatusCode.FORBIDDEN_403)
+      await servers[1].videos.get({ id, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     }
   })
 
   after(async function () {
-    await closeAllSequelize([ servers[1] ])
-
     await cleanupTests(servers)
   })
 })
index e8ba89ca67612eafd69580cbf1fff4060b33f055..cb3ba5677833e1cab83a6130984ef53d19fa8b09 100644 (file)
@@ -2,51 +2,48 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { HttpStatusCode } from '@shared/core-utils'
-import { CustomPage, ServerConfig } from '@shared/models'
+import { HttpStatusCode } from '@shared/models'
 import {
   cleanupTests,
-  flushAndRunServer,
-  getConfig,
-  getInstanceHomepage,
+  createSingleServer,
+  CustomPagesCommand,
   killallServers,
-  reRunServer,
-  ServerInfo,
-  setAccessTokensToServers,
-  updateInstanceHomepage
+  PeerTubeServer,
+  setAccessTokensToServers
 } from '../../../../shared/extra-utils/index'
 
 const expect = chai.expect
 
-async function getHomepageState (server: ServerInfo) {
-  const res = await getConfig(server.url)
+async function getHomepageState (server: PeerTubeServer) {
+  const config = await server.config.getConfig()
 
-  const config = res.body as ServerConfig
   return config.homepage.enabled
 }
 
 describe('Test instance homepage actions', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
+  let command: CustomPagesCommand
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
+
+    command = server.customPage
   })
 
   it('Should not have a homepage', async function () {
     const state = await getHomepageState(server)
     expect(state).to.be.false
 
-    await getInstanceHomepage(server.url, HttpStatusCode.NOT_FOUND_404)
+    await command.getInstanceHomepage({ expectedStatus: HttpStatusCode.NOT_FOUND_404 })
   })
 
   it('Should set a homepage', async function () {
-    await updateInstanceHomepage(server.url, server.accessToken, '<picsou-magazine></picsou-magazine>')
+    await command.updateInstanceHomepage({ content: '<picsou-magazine></picsou-magazine>' })
 
-    const res = await getInstanceHomepage(server.url)
-    const page: CustomPage = res.body
+    const page = await command.getInstanceHomepage()
     expect(page.content).to.equal('<picsou-magazine></picsou-magazine>')
 
     const state = await getHomepageState(server)
@@ -56,12 +53,11 @@ describe('Test instance homepage actions', function () {
   it('Should have the same homepage after a restart', async function () {
     this.timeout(30000)
 
-    killallServers([ server ])
+    await killallServers([ server ])
 
-    await reRunServer(server)
+    await server.run()
 
-    const res = await getInstanceHomepage(server.url)
-    const page: CustomPage = res.body
+    const page = await command.getInstanceHomepage()
     expect(page.content).to.equal('<picsou-magazine></picsou-magazine>')
 
     const state = await getHomepageState(server)
@@ -69,10 +65,9 @@ describe('Test instance homepage actions', function () {
   })
 
   it('Should empty the homepage', async function () {
-    await updateInstanceHomepage(server.url, server.accessToken, '')
+    await command.updateInstanceHomepage({ content: '' })
 
-    const res = await getInstanceHomepage(server.url)
-    const page: CustomPage = res.body
+    const page = await command.getInstanceHomepage()
     expect(page.content).to.be.empty
 
     const state = await getHomepageState(server)
index d0e222997645dad4cfce802fad172fd38144d6a1..c10c154c2a52586d9f7623f4ead38d7613250a32 100644 (file)
@@ -1,24 +1,26 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { cleanupTests, ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
-import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
-import { getJobsList, getJobsListPaginationAndSort, waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { flushAndRunMultipleServers } from '../../../../shared/extra-utils/server/servers'
-import { uploadVideo } from '../../../../shared/extra-utils/videos/videos'
-import { dateIsValid } from '../../../../shared/extra-utils/miscs/miscs'
-import { Job } from '../../../../shared/models/server'
+import * as chai from 'chai'
+import {
+  cleanupTests,
+  createMultipleServers,
+  dateIsValid,
+  doubleFollow,
+  PeerTubeServer,
+  setAccessTokensToServers,
+  waitJobs
+} from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test jobs', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
 
   before(async function () {
     this.timeout(30000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
 
@@ -27,36 +29,34 @@ describe('Test jobs', function () {
   })
 
   it('Should create some jobs', async function () {
-    this.timeout(60000)
+    this.timeout(120000)
 
-    await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' })
-    await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' })
+    await servers[1].videos.upload({ attributes: { name: 'video1' } })
+    await servers[1].videos.upload({ attributes: { name: 'video2' } })
 
     await waitJobs(servers)
   })
 
   it('Should list jobs', async function () {
-    const res = await getJobsList(servers[1].url, servers[1].accessToken, 'completed')
-    expect(res.body.total).to.be.above(2)
-    expect(res.body.data).to.have.length.above(2)
+    const body = await servers[1].jobs.getJobsList({ state: 'completed' })
+    expect(body.total).to.be.above(2)
+    expect(body.data).to.have.length.above(2)
   })
 
   it('Should list jobs with sort, pagination and job type', async function () {
     {
-      const res = await getJobsListPaginationAndSort({
-        url: servers[1].url,
-        accessToken: servers[1].accessToken,
+      const body = await servers[1].jobs.getJobsList({
         state: 'completed',
         start: 1,
         count: 2,
         sort: 'createdAt'
       })
-      expect(res.body.total).to.be.above(2)
-      expect(res.body.data).to.have.lengthOf(2)
+      expect(body.total).to.be.above(2)
+      expect(body.data).to.have.lengthOf(2)
 
-      let job: Job = res.body.data[0]
+      let job = body.data[0]
       // Skip repeat jobs
-      if (job.type === 'videos-views') job = res.body.data[1]
+      if (job.type === 'videos-views') job = body.data[1]
 
       expect(job.state).to.equal('completed')
       expect(job.type.startsWith('activitypub-')).to.be.true
@@ -66,29 +66,26 @@ describe('Test jobs', function () {
     }
 
     {
-      const res = await getJobsListPaginationAndSort({
-        url: servers[1].url,
-        accessToken: servers[1].accessToken,
+      const body = await servers[1].jobs.getJobsList({
         state: 'completed',
         start: 0,
         count: 100,
         sort: 'createdAt',
         jobType: 'activitypub-http-broadcast'
       })
-      expect(res.body.total).to.be.above(2)
+      expect(body.total).to.be.above(2)
 
-      for (const j of res.body.data as Job[]) {
+      for (const j of body.data) {
         expect(j.type).to.equal('activitypub-http-broadcast')
       }
     }
   })
 
   it('Should list all jobs', async function () {
-    const res = await getJobsList(servers[1].url, servers[1].accessToken)
-
-    const jobs = res.body.data as Job[]
+    const body = await servers[1].jobs.getJobsList()
+    expect(body.total).to.be.above(2)
 
-    expect(res.body.total).to.be.above(2)
+    const jobs = body.data
     expect(jobs).to.have.length.above(2)
 
     // We know there are a least 1 delayed job (video views) and 1 completed job (broadcast)
index bc398ea731ac04bd8479fa1114703edb62c11a91..bcd94dda30618e6310d31c774e372b16d51c4fd3 100644 (file)
@@ -4,27 +4,27 @@ import 'mocha'
 import * as chai from 'chai'
 import {
   cleanupTests,
-  flushAndRunServer,
+  createSingleServer,
   killallServers,
-  makePingRequest,
-  reRunServer,
-  ServerInfo,
-  setAccessTokensToServers
-} from '../../../../shared/extra-utils/index'
-import { getAuditLogs, getLogs } from '../../../../shared/extra-utils/logs/logs'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { uploadVideo } from '../../../../shared/extra-utils/videos/videos'
+  LogsCommand,
+  PeerTubeServer,
+  setAccessTokensToServers,
+  waitJobs
+} from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test logs', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
+  let logsCommand: LogsCommand
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
+
+    logsCommand = server.logs
   })
 
   describe('With the standard log file', function () {
@@ -32,16 +32,16 @@ describe('Test logs', function () {
     it('Should get logs with a start date', async function () {
       this.timeout(20000)
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
+      await server.videos.upload({ attributes: { name: 'video 1' } })
       await waitJobs([ server ])
 
       const now = new Date()
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
+      await server.videos.upload({ attributes: { name: 'video 2' } })
       await waitJobs([ server ])
 
-      const res = await getLogs(server.url, server.accessToken, now)
-      const logsString = JSON.stringify(res.body)
+      const body = await logsCommand.getLogs({ startDate: now })
+      const logsString = JSON.stringify(body)
 
       expect(logsString.includes('video 1')).to.be.false
       expect(logsString.includes('video 2')).to.be.true
@@ -50,21 +50,21 @@ describe('Test logs', function () {
     it('Should get logs with an end date', async function () {
       this.timeout(30000)
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
+      await server.videos.upload({ attributes: { name: 'video 3' } })
       await waitJobs([ server ])
 
       const now1 = new Date()
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 4' })
+      await server.videos.upload({ attributes: { name: 'video 4' } })
       await waitJobs([ server ])
 
       const now2 = new Date()
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 5' })
+      await server.videos.upload({ attributes: { name: 'video 5' } })
       await waitJobs([ server ])
 
-      const res = await getLogs(server.url, server.accessToken, now1, now2)
-      const logsString = JSON.stringify(res.body)
+      const body = await logsCommand.getLogs({ startDate: now1, endDate: now2 })
+      const logsString = JSON.stringify(body)
 
       expect(logsString.includes('video 3')).to.be.false
       expect(logsString.includes('video 4')).to.be.true
@@ -76,19 +76,19 @@ describe('Test logs', function () {
 
       const now = new Date()
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 6' })
+      await server.videos.upload({ attributes: { name: 'video 6' } })
       await waitJobs([ server ])
 
       {
-        const res = await getLogs(server.url, server.accessToken, now, undefined, 'info')
-        const logsString = JSON.stringify(res.body)
+        const body = await logsCommand.getLogs({ startDate: now, level: 'info' })
+        const logsString = JSON.stringify(body)
 
         expect(logsString.includes('video 6')).to.be.true
       }
 
       {
-        const res = await getLogs(server.url, server.accessToken, now, undefined, 'warn')
-        const logsString = JSON.stringify(res.body)
+        const body = await logsCommand.getLogs({ startDate: now, level: 'warn' })
+        const logsString = JSON.stringify(body)
 
         expect(logsString.includes('video 6')).to.be.false
       }
@@ -99,10 +99,10 @@ describe('Test logs', function () {
 
       const now = new Date()
 
-      await makePingRequest(server)
+      await server.servers.ping()
 
-      const res = await getLogs(server.url, server.accessToken, now, undefined, 'info')
-      const logsString = JSON.stringify(res.body)
+      const body = await logsCommand.getLogs({ startDate: now, level: 'info' })
+      const logsString = JSON.stringify(body)
 
       expect(logsString.includes('/api/v1/ping')).to.be.true
     })
@@ -110,16 +110,16 @@ describe('Test logs', function () {
     it('Should not log ping requests', async function () {
       this.timeout(30000)
 
-      killallServers([ server ])
+      await killallServers([ server ])
 
-      await reRunServer(server, { log: { log_ping_requests: false } })
+      await server.run({ log: { log_ping_requests: false } })
 
       const now = new Date()
 
-      await makePingRequest(server)
+      await server.servers.ping()
 
-      const res = await getLogs(server.url, server.accessToken, now, undefined, 'info')
-      const logsString = JSON.stringify(res.body)
+      const body = await logsCommand.getLogs({ startDate: now, level: 'info' })
+      const logsString = JSON.stringify(body)
 
       expect(logsString.includes('/api/v1/ping')).to.be.false
     })
@@ -129,23 +129,23 @@ describe('Test logs', function () {
     it('Should get logs with a start date', async function () {
       this.timeout(20000)
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 7' })
+      await server.videos.upload({ attributes: { name: 'video 7' } })
       await waitJobs([ server ])
 
       const now = new Date()
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 8' })
+      await server.videos.upload({ attributes: { name: 'video 8' } })
       await waitJobs([ server ])
 
-      const res = await getAuditLogs(server.url, server.accessToken, now)
-      const logsString = JSON.stringify(res.body)
+      const body = await logsCommand.getAuditLogs({ startDate: now })
+      const logsString = JSON.stringify(body)
 
       expect(logsString.includes('video 7')).to.be.false
       expect(logsString.includes('video 8')).to.be.true
 
-      expect(res.body).to.have.lengthOf(1)
+      expect(body).to.have.lengthOf(1)
 
-      const item = res.body[0]
+      const item = body[0]
 
       const message = JSON.parse(item.message)
       expect(message.domain).to.equal('videos')
@@ -155,21 +155,21 @@ describe('Test logs', function () {
     it('Should get logs with an end date', async function () {
       this.timeout(30000)
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 9' })
+      await server.videos.upload({ attributes: { name: 'video 9' } })
       await waitJobs([ server ])
 
       const now1 = new Date()
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 10' })
+      await server.videos.upload({ attributes: { name: 'video 10' } })
       await waitJobs([ server ])
 
       const now2 = new Date()
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 11' })
+      await server.videos.upload({ attributes: { name: 'video 11' } })
       await waitJobs([ server ])
 
-      const res = await getAuditLogs(server.url, server.accessToken, now1, now2)
-      const logsString = JSON.stringify(res.body)
+      const body = await logsCommand.getAuditLogs({ startDate: now1, endDate: now2 })
+      const logsString = JSON.stringify(body)
 
       expect(logsString.includes('video 9')).to.be.false
       expect(logsString.includes('video 10')).to.be.true
index d589f51f3e6ef1b6ccd4013fb32f3cf4666bb724..719813ae904a602115b221903a8fc0a32f00f812 100644 (file)
@@ -1,16 +1,15 @@
 import 'mocha'
 import * as request from 'supertest'
-import { ServerInfo } from '../../../../shared/extra-utils'
-import { cleanupTests, flushAndRunServer } from '../../../../shared/extra-utils/server/servers'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { cleanupTests, createSingleServer, PeerTubeServer } from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Start and stop server without web client routes', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1, {}, [ '--no-client' ])
+    server = await createSingleServer(1, {}, { peertubeArgs: [ '--no-client' ] })
   })
 
   it('Should fail getting the client', function () {
index 6b61c7c33705384733584872d4fe6d7d7daa62f0..5f9f4ffddf99bd8f27036e1208a4448aa9dda020 100644 (file)
@@ -2,41 +2,23 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { HttpStatusCode } from '@shared/core-utils'
 import {
   cleanupTests,
-  closeAllSequelize,
-  flushAndRunServer,
-  getConfig,
-  getMyUserInformation,
-  getPlugin,
-  getPluginPackageJSON,
-  getPluginTestPath,
-  getPublicSettings,
-  installPlugin,
+  createSingleServer,
   killallServers,
-  listAvailablePlugins,
-  listPlugins,
-  reRunServer,
-  ServerInfo,
+  PeerTubeServer,
+  PluginsCommand,
   setAccessTokensToServers,
-  setPluginVersion,
   testHelloWorldRegisteredSettings,
-  uninstallPlugin,
-  updateCustomSubConfig,
-  updateMyUser,
-  updatePlugin,
-  updatePluginPackageJSON,
-  updatePluginSettings,
-  wait,
-  waitUntilLog
+  wait
 } from '@shared/extra-utils'
-import { PeerTubePlugin, PeerTubePluginIndex, PluginPackageJson, PluginType, PublicServerSetting, ServerConfig, User } from '@shared/models'
+import { HttpStatusCode, PluginType } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test plugins', function () {
-  let server: ServerInfo = null
+  let server: PeerTubeServer = null
+  let command: PluginsCommand
 
   before(async function () {
     this.timeout(30000)
@@ -46,68 +28,61 @@ describe('Test plugins', function () {
         index: { check_latest_versions_interval: '5 seconds' }
       }
     }
-    server = await flushAndRunServer(1, configOverride)
+    server = await createSingleServer(1, configOverride)
     await setAccessTokensToServers([ server ])
+
+    command = server.plugins
   })
 
   it('Should list and search available plugins and themes', async function () {
     this.timeout(30000)
 
     {
-      const res = await listAvailablePlugins({
-        url: server.url,
-        accessToken: server.accessToken,
+      const body = await command.listAvailable({
         count: 1,
         start: 0,
         pluginType: PluginType.THEME,
         search: 'background-red'
       })
 
-      expect(res.body.total).to.be.at.least(1)
-      expect(res.body.data).to.have.lengthOf(1)
+      expect(body.total).to.be.at.least(1)
+      expect(body.data).to.have.lengthOf(1)
     }
 
     {
-      const res1 = await listAvailablePlugins({
-        url: server.url,
-        accessToken: server.accessToken,
+      const body1 = await command.listAvailable({
         count: 2,
         start: 0,
         sort: 'npmName'
       })
-      const data1: PeerTubePluginIndex[] = res1.body.data
+      expect(body1.total).to.be.at.least(2)
 
-      expect(res1.body.total).to.be.at.least(2)
+      const data1 = body1.data
       expect(data1).to.have.lengthOf(2)
 
-      const res2 = await listAvailablePlugins({
-        url: server.url,
-        accessToken: server.accessToken,
+      const body2 = await command.listAvailable({
         count: 2,
         start: 0,
         sort: '-npmName'
       })
-      const data2: PeerTubePluginIndex[] = res2.body.data
+      expect(body2.total).to.be.at.least(2)
 
-      expect(res2.body.total).to.be.at.least(2)
+      const data2 = body2.data
       expect(data2).to.have.lengthOf(2)
 
       expect(data1[0].npmName).to.not.equal(data2[0].npmName)
     }
 
     {
-      const res = await listAvailablePlugins({
-        url: server.url,
-        accessToken: server.accessToken,
+      const body = await command.listAvailable({
         count: 10,
         start: 0,
         pluginType: PluginType.THEME,
         search: 'background-red',
         currentPeerTubeEngine: '1.0.0'
       })
-      const data: PeerTubePluginIndex[] = res.body.data
 
-      const p = data.find(p => p.npmName === 'peertube-theme-background-red')
+      const p = body.data.find(p => p.npmName === 'peertube-theme-background-red')
       expect(p).to.be.undefined
     }
   })
@@ -115,22 +90,12 @@ describe('Test plugins', function () {
   it('Should install a plugin and a theme', async function () {
     this.timeout(30000)
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-plugin-hello-world'
-    })
-
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-theme-background-red'
-    })
+    await command.install({ npmName: 'peertube-plugin-hello-world' })
+    await command.install({ npmName: 'peertube-theme-background-red' })
   })
 
   it('Should have the plugin loaded in the configuration', async function () {
-    const res = await getConfig(server.url)
-    const config: ServerConfig = res.body
+    const config = await server.config.getConfig()
 
     const theme = config.theme.registered.find(r => r.name === 'background-red')
     expect(theme).to.not.be.undefined
@@ -140,66 +105,56 @@ describe('Test plugins', function () {
   })
 
   it('Should update the default theme in the configuration', async function () {
-    await updateCustomSubConfig(server.url, server.accessToken, { theme: { default: 'background-red' } })
-
-    const res = await getConfig(server.url)
-    const config: ServerConfig = res.body
+    await server.config.updateCustomSubConfig({
+      newConfig: {
+        theme: { default: 'background-red' }
+      }
+    })
 
+    const config = await server.config.getConfig()
     expect(config.theme.default).to.equal('background-red')
   })
 
   it('Should update my default theme', async function () {
-    await updateMyUser({
-      url: server.url,
-      accessToken: server.accessToken,
-      theme: 'background-red'
-    })
+    await server.users.updateMe({ theme: 'background-red' })
 
-    const res = await getMyUserInformation(server.url, server.accessToken)
-    expect((res.body as User).theme).to.equal('background-red')
+    const user = await server.users.getMyInfo()
+    expect(user.theme).to.equal('background-red')
   })
 
   it('Should list plugins and themes', async function () {
     {
-      const res = await listPlugins({
-        url: server.url,
-        accessToken: server.accessToken,
+      const body = await command.list({
         count: 1,
         start: 0,
         pluginType: PluginType.THEME
       })
-      const data: PeerTubePlugin[] = res.body.data
+      expect(body.total).to.be.at.least(1)
 
-      expect(res.body.total).to.be.at.least(1)
+      const data = body.data
       expect(data).to.have.lengthOf(1)
       expect(data[0].name).to.equal('background-red')
     }
 
     {
-      const res = await listPlugins({
-        url: server.url,
-        accessToken: server.accessToken,
+      const { data } = await command.list({
         count: 2,
         start: 0,
         sort: 'name'
       })
-      const data: PeerTubePlugin[] = res.body.data
 
       expect(data[0].name).to.equal('background-red')
       expect(data[1].name).to.equal('hello-world')
     }
 
     {
-      const res = await listPlugins({
-        url: server.url,
-        accessToken: server.accessToken,
+      const body = await command.list({
         count: 2,
         start: 1,
         sort: 'name'
       })
-      const data: PeerTubePlugin[] = res.body.data
 
-      expect(data[0].name).to.equal('hello-world')
+      expect(body.data[0].name).to.equal('hello-world')
     }
   })
 
@@ -208,9 +163,8 @@ describe('Test plugins', function () {
   })
 
   it('Should get public settings', async function () {
-    const res = await getPublicSettings({ url: server.url, npmName: 'peertube-plugin-hello-world' })
-
-    const publicSettings = (res.body as PublicServerSetting).publicSettings
+    const body = await command.getPublicSettings({ npmName: 'peertube-plugin-hello-world' })
+    const publicSettings = body.publicSettings
 
     expect(Object.keys(publicSettings)).to.have.lengthOf(1)
     expect(Object.keys(publicSettings)).to.deep.equal([ 'user-name' ])
@@ -222,9 +176,7 @@ describe('Test plugins', function () {
       'admin-name': 'Cid'
     }
 
-    await updatePluginSettings({
-      url: server.url,
-      accessToken: server.accessToken,
+    await command.updateSettings({
       npmName: 'peertube-plugin-hello-world',
       settings
     })
@@ -233,18 +185,12 @@ describe('Test plugins', function () {
   it('Should have watched settings changes', async function () {
     this.timeout(10000)
 
-    await waitUntilLog(server, 'Settings changed!')
+    await server.servers.waitUntilLog('Settings changed!')
   })
 
   it('Should get a plugin and a theme', async function () {
     {
-      const res = await getPlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        npmName: 'peertube-plugin-hello-world'
-      })
-
-      const plugin: PeerTubePlugin = res.body
+      const plugin = await command.get({ npmName: 'peertube-plugin-hello-world' })
 
       expect(plugin.type).to.equal(PluginType.PLUGIN)
       expect(plugin.name).to.equal('hello-world')
@@ -262,13 +208,7 @@ describe('Test plugins', function () {
     }
 
     {
-      const res = await getPlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        npmName: 'peertube-theme-background-red'
-      })
-
-      const plugin: PeerTubePlugin = res.body
+      const plugin = await command.get({ npmName: 'peertube-theme-background-red' })
 
       expect(plugin.type).to.equal(PluginType.THEME)
       expect(plugin.name).to.equal('background-red')
@@ -292,101 +232,66 @@ describe('Test plugins', function () {
     await wait(6000)
 
     // Fake update our plugin version
-    await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+    await server.sql.setPluginVersion('hello-world', '0.0.1')
 
     // Fake update package.json
-    const packageJSON: PluginPackageJson = await getPluginPackageJSON(server, 'peertube-plugin-hello-world')
+    const packageJSON = await command.getPackageJSON('peertube-plugin-hello-world')
     const oldVersion = packageJSON.version
 
     packageJSON.version = '0.0.1'
-    await updatePluginPackageJSON(server, 'peertube-plugin-hello-world', packageJSON)
+    await command.updatePackageJSON('peertube-plugin-hello-world', packageJSON)
 
     // Restart the server to take into account this change
-    killallServers([ server ])
-    await reRunServer(server)
+    await killallServers([ server ])
+    await server.run()
 
     {
-      const res = await listPlugins({
-        url: server.url,
-        accessToken: server.accessToken,
-        pluginType: PluginType.PLUGIN
-      })
-
-      const plugin: PeerTubePlugin = res.body.data[0]
+      const body = await command.list({ pluginType: PluginType.PLUGIN })
 
+      const plugin = body.data[0]
       expect(plugin.version).to.equal('0.0.1')
       expect(plugin.latestVersion).to.exist
       expect(plugin.latestVersion).to.not.equal('0.0.1')
     }
 
     {
-      await updatePlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        npmName: 'peertube-plugin-hello-world'
-      })
-
-      const res = await listPlugins({
-        url: server.url,
-        accessToken: server.accessToken,
-        pluginType: PluginType.PLUGIN
-      })
+      await command.update({ npmName: 'peertube-plugin-hello-world' })
 
-      const plugin: PeerTubePlugin = res.body.data[0]
+      const body = await command.list({ pluginType: PluginType.PLUGIN })
 
+      const plugin = body.data[0]
       expect(plugin.version).to.equal(oldVersion)
 
-      const updatedPackageJSON: PluginPackageJson = await getPluginPackageJSON(server, 'peertube-plugin-hello-world')
+      const updatedPackageJSON = await command.getPackageJSON('peertube-plugin-hello-world')
       expect(updatedPackageJSON.version).to.equal(oldVersion)
     }
   })
 
   it('Should uninstall the plugin', async function () {
-    await uninstallPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-plugin-hello-world'
-    })
-
-    const res = await listPlugins({
-      url: server.url,
-      accessToken: server.accessToken,
-      pluginType: PluginType.PLUGIN
-    })
+    await command.uninstall({ npmName: 'peertube-plugin-hello-world' })
 
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    const body = await command.list({ pluginType: PluginType.PLUGIN })
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   it('Should list uninstalled plugins', async function () {
-    const res = await listPlugins({
-      url: server.url,
-      accessToken: server.accessToken,
-      pluginType: PluginType.PLUGIN,
-      uninstalled: true
-    })
-
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.have.lengthOf(1)
+    const body = await command.list({ pluginType: PluginType.PLUGIN, uninstalled: true })
+    expect(body.total).to.equal(1)
+    expect(body.data).to.have.lengthOf(1)
 
-    const plugin: PeerTubePlugin = res.body.data[0]
+    const plugin = body.data[0]
     expect(plugin.name).to.equal('hello-world')
     expect(plugin.enabled).to.be.false
     expect(plugin.uninstalled).to.be.true
   })
 
   it('Should uninstall the theme', async function () {
-    await uninstallPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-theme-background-red'
-    })
+    await command.uninstall({ npmName: 'peertube-theme-background-red' })
   })
 
   it('Should have updated the configuration', async function () {
-    // get /config (default theme + registered themes + registered plugins)
-    const res = await getConfig(server.url)
-    const config: ServerConfig = res.body
+    const config = await server.config.getConfig()
 
     expect(config.theme.default).to.equal('default')
 
@@ -398,42 +303,33 @@ describe('Test plugins', function () {
   })
 
   it('Should have updated the user theme', async function () {
-    const res = await getMyUserInformation(server.url, server.accessToken)
-    expect((res.body as User).theme).to.equal('instance-default')
+    const user = await server.users.getMyInfo()
+    expect(user.theme).to.equal('instance-default')
   })
 
   it('Should not install a broken plugin', async function () {
     this.timeout(60000)
 
     async function check () {
-      const res = await listPlugins({
-        url: server.url,
-        accessToken: server.accessToken,
-        pluginType: PluginType.PLUGIN
-      })
-
-      const plugins: PeerTubePlugin[] = res.body.data
-
+      const body = await command.list({ pluginType: PluginType.PLUGIN })
+      const plugins = body.data
       expect(plugins.find(p => p.name === 'test-broken')).to.not.exist
     }
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      path: getPluginTestPath('-broken'),
+    await command.install({
+      path: PluginsCommand.getPluginTestPath('-broken'),
       expectedStatus: HttpStatusCode.BAD_REQUEST_400
     })
 
     await check()
 
-    killallServers([ server ])
-    await reRunServer(server)
+    await killallServers([ server ])
+    await server.run()
 
     await check()
   })
 
   after(async function () {
-    await closeAllSequelize([ server ])
     await cleanupTests([ server ])
   })
 })
index 17d1ee4a573e47ae0a51417b80746faee8fdaf1c..484f88d674a89da3d16d164763f8ce161eacd740 100644 (file)
@@ -1,16 +1,12 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import 'mocha'
-import * as chai from 'chai'
-import { cleanupTests, getVideo, registerUser, uploadVideo, userLogin, viewVideo, wait } from '../../../../shared/extra-utils'
-import { flushAndRunServer, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-
-const expect = chai.expect
+import { expect } from 'chai'
+import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers, wait } from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test application behind a reverse proxy', function () {
-  let server = null
-  let videoId
+  let server: PeerTubeServer
+  let videoId: string
 
   before(async function () {
     this.timeout(30000)
@@ -34,85 +30,85 @@ describe('Test application behind a reverse proxy', function () {
       }
     }
 
-    server = await flushAndRunServer(1, config)
+    server = await createSingleServer(1, config)
     await setAccessTokensToServers([ server ])
 
-    const { body } = await uploadVideo(server.url, server.accessToken, {})
-    videoId = body.video.uuid
+    const { uuid } = await server.videos.upload()
+    videoId = uuid
   })
 
   it('Should view a video only once with the same IP by default', async function () {
     this.timeout(20000)
 
-    await viewVideo(server.url, videoId)
-    await viewVideo(server.url, videoId)
+    await server.videos.view({ id: videoId })
+    await server.videos.view({ id: videoId })
 
     // Wait the repeatable job
     await wait(8000)
 
-    const { body } = await getVideo(server.url, videoId)
-    expect(body.views).to.equal(1)
+    const video = await server.videos.get({ id: videoId })
+    expect(video.views).to.equal(1)
   })
 
   it('Should view a video 2 times with the X-Forwarded-For header set', async function () {
     this.timeout(20000)
 
-    await viewVideo(server.url, videoId, HttpStatusCode.NO_CONTENT_204, '0.0.0.1,127.0.0.1')
-    await viewVideo(server.url, videoId, HttpStatusCode.NO_CONTENT_204, '0.0.0.2,127.0.0.1')
+    await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.1,127.0.0.1' })
+    await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.2,127.0.0.1' })
 
     // Wait the repeatable job
     await wait(8000)
 
-    const { body } = await getVideo(server.url, videoId)
-    expect(body.views).to.equal(3)
+    const video = await server.videos.get({ id: videoId })
+    expect(video.views).to.equal(3)
   })
 
   it('Should view a video only once with the same client IP in the X-Forwarded-For header', async function () {
     this.timeout(20000)
 
-    await viewVideo(server.url, videoId, HttpStatusCode.NO_CONTENT_204, '0.0.0.4,0.0.0.3,::ffff:127.0.0.1')
-    await viewVideo(server.url, videoId, HttpStatusCode.NO_CONTENT_204, '0.0.0.5,0.0.0.3,127.0.0.1')
+    await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.4,0.0.0.3,::ffff:127.0.0.1' })
+    await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.5,0.0.0.3,127.0.0.1' })
 
     // Wait the repeatable job
     await wait(8000)
 
-    const { body } = await getVideo(server.url, videoId)
-    expect(body.views).to.equal(4)
+    const video = await server.videos.get({ id: videoId })
+    expect(video.views).to.equal(4)
   })
 
   it('Should view a video two times with a different client IP in the X-Forwarded-For header', async function () {
     this.timeout(20000)
 
-    await viewVideo(server.url, videoId, HttpStatusCode.NO_CONTENT_204, '0.0.0.8,0.0.0.6,127.0.0.1')
-    await viewVideo(server.url, videoId, HttpStatusCode.NO_CONTENT_204, '0.0.0.8,0.0.0.7,127.0.0.1')
+    await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.6,127.0.0.1' })
+    await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.7,127.0.0.1' })
 
     // Wait the repeatable job
     await wait(8000)
 
-    const { body } = await getVideo(server.url, videoId)
-    expect(body.views).to.equal(6)
+    const video = await server.videos.get({ id: videoId })
+    expect(video.views).to.equal(6)
   })
 
   it('Should rate limit logins', async function () {
     const user = { username: 'root', password: 'fail' }
 
     for (let i = 0; i < 19; i++) {
-      await userLogin(server, user, HttpStatusCode.BAD_REQUEST_400)
+      await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     }
 
-    await userLogin(server, user, HttpStatusCode.TOO_MANY_REQUESTS_429)
+    await server.login.login({ user, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
   })
 
   it('Should rate limit signup', async function () {
     for (let i = 0; i < 10; i++) {
       try {
-        await registerUser(server.url, 'test' + i, 'password')
+        await server.users.register({ username: 'test' + i })
       } catch {
         // empty
       }
     }
 
-    await registerUser(server.url, 'test42', 'password', HttpStatusCode.TOO_MANY_REQUESTS_429)
+    await server.users.register({ username: 'test42', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
   })
 
   it('Should not rate limit failed signup', async function () {
@@ -121,10 +117,10 @@ describe('Test application behind a reverse proxy', function () {
     await wait(7000)
 
     for (let i = 0; i < 3; i++) {
-      await registerUser(server.url, 'test' + i, 'password', HttpStatusCode.CONFLICT_409)
+      await server.users.register({ username: 'test' + i, expectedStatus: HttpStatusCode.CONFLICT_409 })
     }
 
-    await registerUser(server.url, 'test43', 'password', HttpStatusCode.NO_CONTENT_204)
+    await server.users.register({ username: 'test43', expectedStatus: HttpStatusCode.NO_CONTENT_204 })
 
   })
 
@@ -135,13 +131,13 @@ describe('Test application behind a reverse proxy', function () {
 
     for (let i = 0; i < 100; i++) {
       try {
-        await getVideo(server.url, videoId)
+        await server.videos.get({ id: videoId })
       } catch {
         // don't care if it fails
       }
     }
 
-    await getVideo(server.url, videoId, HttpStatusCode.TOO_MANY_REQUESTS_429)
+    await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
   })
 
   after(async function () {
index ea64e4040b79693b6c36319e3344e05b217d5e77..69d030dbb8d4a585adb7af0a871464e909c4153d 100644 (file)
@@ -2,23 +2,13 @@
 
 import 'mocha'
 import * as chai from 'chai'
+import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers, setDefaultVideoChannel } from '@shared/extra-utils'
 import { Video, VideoPlaylistPrivacy } from '@shared/models'
-import {
-  addVideoInPlaylist,
-  createVideoPlaylist,
-  getOEmbed,
-  getVideosList,
-  ServerInfo,
-  setAccessTokensToServers,
-  setDefaultVideoChannel,
-  uploadVideo
-} from '../../../../shared/extra-utils'
-import { cleanupTests, flushAndRunServer } from '../../../../shared/extra-utils/server/servers'
 
 const expect = chai.expect
 
 describe('Test services', function () {
-  let server: ServerInfo = null
+  let server: PeerTubeServer = null
   let playlistUUID: string
   let playlistDisplayName: string
   let video: Video
@@ -26,40 +16,34 @@ describe('Test services', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
     await setDefaultVideoChannel([ server ])
 
     {
-      const videoAttributes = {
-        name: 'my super name'
-      }
-      await uploadVideo(server.url, server.accessToken, videoAttributes)
+      const attributes = { name: 'my super name' }
+      await server.videos.upload({ attributes })
 
-      const res = await getVideosList(server.url)
-      video = res.body.data[0]
+      const { data } = await server.videos.list()
+      video = data[0]
     }
 
     {
-      const res = await createVideoPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
+      const created = await server.playlists.create({
+        attributes: {
           displayName: 'The Life and Times of Scrooge McDuck',
           privacy: VideoPlaylistPrivacy.PUBLIC,
-          videoChannelId: server.videoChannel.id
+          videoChannelId: server.store.channel.id
         }
       })
 
-      playlistUUID = res.body.videoPlaylist.uuid
+      playlistUUID = created.uuid
       playlistDisplayName = 'The Life and Times of Scrooge McDuck'
 
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: res.body.videoPlaylist.id,
-        elementAttrs: {
+      await server.playlists.addElement({
+        playlistId: created.id,
+        attributes: {
           videoId: video.id
         }
       })
@@ -70,7 +54,7 @@ describe('Test services', function () {
     for (const basePath of [ '/videos/watch/', '/w/' ]) {
       const oembedUrl = 'http://localhost:' + server.port + basePath + video.uuid
 
-      const res = await getOEmbed(server.url, oembedUrl)
+      const res = await server.services.getOEmbed({ oembedUrl })
       const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
         `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` +
         'frameborder="0" allowfullscreen></iframe>'
@@ -78,7 +62,7 @@ describe('Test services', function () {
 
       expect(res.body.html).to.equal(expectedHtml)
       expect(res.body.title).to.equal(video.name)
-      expect(res.body.author_name).to.equal(server.videoChannel.displayName)
+      expect(res.body.author_name).to.equal(server.store.channel.displayName)
       expect(res.body.width).to.equal(560)
       expect(res.body.height).to.equal(315)
       expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl)
@@ -91,14 +75,14 @@ describe('Test services', function () {
     for (const basePath of [ '/videos/watch/playlist/', '/w/p/' ]) {
       const oembedUrl = 'http://localhost:' + server.port + basePath + playlistUUID
 
-      const res = await getOEmbed(server.url, oembedUrl)
+      const res = await server.services.getOEmbed({ oembedUrl })
       const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
         `title="${playlistDisplayName}" src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` +
         'frameborder="0" allowfullscreen></iframe>'
 
       expect(res.body.html).to.equal(expectedHtml)
       expect(res.body.title).to.equal('The Life and Times of Scrooge McDuck')
-      expect(res.body.author_name).to.equal(server.videoChannel.displayName)
+      expect(res.body.author_name).to.equal(server.store.channel.displayName)
       expect(res.body.width).to.equal(560)
       expect(res.body.height).to.equal(315)
       expect(res.body.thumbnail_url).exist
@@ -114,14 +98,14 @@ describe('Test services', function () {
       const maxHeight = 50
       const maxWidth = 50
 
-      const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth)
+      const res = await server.services.getOEmbed({ oembedUrl, format, maxHeight, maxWidth })
       const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' +
         `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` +
         'frameborder="0" allowfullscreen></iframe>'
 
       expect(res.body.html).to.equal(expectedHtml)
       expect(res.body.title).to.equal(video.name)
-      expect(res.body.author_name).to.equal(server.videoChannel.displayName)
+      expect(res.body.author_name).to.equal(server.store.channel.displayName)
       expect(res.body.height).to.equal(50)
       expect(res.body.width).to.equal(50)
       expect(res.body).to.not.have.property('thumbnail_url')
index 304181a6d5e6765efbd641be53a91efc5b77746d..5ec771429b41737e115b1701bf7cf293975b4dee 100644 (file)
@@ -3,33 +3,20 @@
 import 'mocha'
 import * as chai from 'chai'
 import {
-  addVideoChannel,
   cleanupTests,
-  createUser,
-  createVideoPlaylist,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  follow,
-  ServerInfo,
-  unfollow,
-  updateCustomSubConfig,
-  uploadVideo,
-  userLogin,
-  viewVideo,
-  wait
-} from '../../../../shared/extra-utils'
-import { setAccessTokensToServers } from '../../../../shared/extra-utils/index'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { getStats } from '../../../../shared/extra-utils/server/stats'
-import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
-import { ServerStats } from '../../../../shared/models/server/server-stats.model'
-import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
-import { ActivityType } from '@shared/models'
+  PeerTubeServer,
+  setAccessTokensToServers,
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { ActivityType, VideoPlaylistPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test stats (excluding redundancy)', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let channelId
   const user = {
     username: 'user1',
@@ -39,31 +26,29 @@ describe('Test stats (excluding redundancy)', function () {
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
 
     await setAccessTokensToServers(servers)
 
     await doubleFollow(servers[0], servers[1])
 
-    await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
+    await servers[0].users.create({ username: user.username, password: user.password })
 
-    const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { fixture: 'video_short.webm' })
-    const videoUUID = resVideo.body.video.uuid
+    const { uuid } = await servers[0].videos.upload({ attributes: { fixture: 'video_short.webm' } })
 
-    await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'comment')
+    await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
 
-    await viewVideo(servers[0].url, videoUUID)
+    await servers[0].videos.view({ id: uuid })
 
     // Wait the video views repeatable job
     await wait(8000)
 
-    await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
+    await servers[2].follows.follow({ hosts: [ servers[0].url ] })
     await waitJobs(servers)
   })
 
   it('Should have the correct stats on instance 1', async function () {
-    const res = await getStats(servers[0].url)
-    const data: ServerStats = res.body
+    const data = await servers[0].stats.get()
 
     expect(data.totalLocalVideoComments).to.equal(1)
     expect(data.totalLocalVideos).to.equal(1)
@@ -78,8 +63,7 @@ describe('Test stats (excluding redundancy)', function () {
   })
 
   it('Should have the correct stats on instance 2', async function () {
-    const res = await getStats(servers[1].url)
-    const data: ServerStats = res.body
+    const data = await servers[1].stats.get()
 
     expect(data.totalLocalVideoComments).to.equal(0)
     expect(data.totalLocalVideos).to.equal(0)
@@ -94,8 +78,7 @@ describe('Test stats (excluding redundancy)', function () {
   })
 
   it('Should have the correct stats on instance 3', async function () {
-    const res = await getStats(servers[2].url)
-    const data: ServerStats = res.body
+    const data = await servers[2].stats.get()
 
     expect(data.totalLocalVideoComments).to.equal(0)
     expect(data.totalLocalVideos).to.equal(0)
@@ -111,11 +94,10 @@ describe('Test stats (excluding redundancy)', function () {
   it('Should have the correct total videos stats after an unfollow', async function () {
     this.timeout(15000)
 
-    await unfollow(servers[2].url, servers[2].accessToken, servers[0])
+    await servers[2].follows.unfollow({ target: servers[0] })
     await waitJobs(servers)
 
-    const res = await getStats(servers[2].url)
-    const data: ServerStats = res.body
+    const data = await servers[2].stats.get()
 
     expect(data.totalVideos).to.equal(0)
   })
@@ -124,18 +106,18 @@ describe('Test stats (excluding redundancy)', function () {
     const server = servers[0]
 
     {
-      const res = await getStats(server.url)
-      const data: ServerStats = res.body
+      const data = await server.stats.get()
+
       expect(data.totalDailyActiveUsers).to.equal(1)
       expect(data.totalWeeklyActiveUsers).to.equal(1)
       expect(data.totalMonthlyActiveUsers).to.equal(1)
     }
 
     {
-      await userLogin(server, user)
+      await server.login.getAccessToken(user)
+
+      const data = await server.stats.get()
 
-      const res = await getStats(server.url)
-      const data: ServerStats = res.body
       expect(data.totalDailyActiveUsers).to.equal(2)
       expect(data.totalWeeklyActiveUsers).to.equal(2)
       expect(data.totalMonthlyActiveUsers).to.equal(2)
@@ -146,33 +128,33 @@ describe('Test stats (excluding redundancy)', function () {
     const server = servers[0]
 
     {
-      const res = await getStats(server.url)
-      const data: ServerStats = res.body
+      const data = await server.stats.get()
+
       expect(data.totalLocalDailyActiveVideoChannels).to.equal(1)
       expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1)
       expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1)
     }
 
     {
-      const channelAttributes = {
+      const attributes = {
         name: 'stats_channel',
         displayName: 'My stats channel'
       }
-      const resChannel = await addVideoChannel(server.url, server.accessToken, channelAttributes)
-      channelId = resChannel.body.videoChannel.id
+      const created = await server.channels.create({ attributes })
+      channelId = created.id
+
+      const data = await server.stats.get()
 
-      const res = await getStats(server.url)
-      const data: ServerStats = res.body
       expect(data.totalLocalDailyActiveVideoChannels).to.equal(1)
       expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1)
       expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1)
     }
 
     {
-      await uploadVideo(server.url, server.accessToken, { fixture: 'video_short.webm', channelId })
+      await server.videos.upload({ attributes: { fixture: 'video_short.webm', channelId } })
+
+      const data = await server.stats.get()
 
-      const res = await getStats(server.url)
-      const data: ServerStats = res.body
       expect(data.totalLocalDailyActiveVideoChannels).to.equal(2)
       expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(2)
       expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(2)
@@ -183,66 +165,62 @@ describe('Test stats (excluding redundancy)', function () {
     const server = servers[0]
 
     {
-      const resStats = await getStats(server.url)
-      const dataStats: ServerStats = resStats.body
-      expect(dataStats.totalLocalPlaylists).to.equal(0)
+      const data = await server.stats.get()
+      expect(data.totalLocalPlaylists).to.equal(0)
     }
 
     {
-      await createVideoPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
+      await server.playlists.create({
+        attributes: {
           displayName: 'playlist for count',
           privacy: VideoPlaylistPrivacy.PUBLIC,
           videoChannelId: channelId
         }
       })
 
-      const resStats = await getStats(server.url)
-      const dataStats: ServerStats = resStats.body
-      expect(dataStats.totalLocalPlaylists).to.equal(1)
+      const data = await server.stats.get()
+      expect(data.totalLocalPlaylists).to.equal(1)
     }
   })
 
   it('Should correctly count video file sizes if transcoding is enabled', async function () {
     this.timeout(60000)
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      transcoding: {
-        enabled: true,
-        webtorrent: {
-          enabled: true
-        },
-        hls: {
-          enabled: true
-        },
-        resolutions: {
-          '0p': false,
-          '240p': false,
-          '360p': false,
-          '480p': false,
-          '720p': false,
-          '1080p': false,
-          '1440p': false,
-          '2160p': false
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        transcoding: {
+          enabled: true,
+          webtorrent: {
+            enabled: true
+          },
+          hls: {
+            enabled: true
+          },
+          resolutions: {
+            '0p': false,
+            '240p': false,
+            '360p': false,
+            '480p': false,
+            '720p': false,
+            '1080p': false,
+            '1440p': false,
+            '2160p': false
+          }
         }
       }
     })
 
-    await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video', fixture: 'video_short.webm' })
+    await servers[0].videos.upload({ attributes: { name: 'video', fixture: 'video_short.webm' } })
 
     await waitJobs(servers)
 
     {
-      const res = await getStats(servers[1].url)
-      const data: ServerStats = res.body
+      const data = await servers[1].stats.get()
       expect(data.totalLocalVideoFilesSize).to.equal(0)
     }
 
     {
-      const res = await getStats(servers[0].url)
-      const data: ServerStats = res.body
+      const data = await servers[0].stats.get()
       expect(data.totalLocalVideoFilesSize).to.be.greaterThan(500000)
       expect(data.totalLocalVideoFilesSize).to.be.lessThan(600000)
     }
@@ -251,27 +229,27 @@ describe('Test stats (excluding redundancy)', function () {
   it('Should have the correct AP stats', async function () {
     this.timeout(60000)
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      transcoding: {
-        enabled: false
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        transcoding: {
+          enabled: false
+        }
       }
     })
 
-    const res1 = await getStats(servers[1].url)
-    const first = res1.body as ServerStats
+    const first = await servers[1].stats.get()
 
     for (let i = 0; i < 10; i++) {
-      await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' })
+      await servers[0].videos.upload({ attributes: { name: 'video' } })
     }
 
     await waitJobs(servers)
 
     await wait(6000)
 
-    const res2 = await getStats(servers[1].url)
-    const second: ServerStats = res2.body
-
+    const second = await servers[1].stats.get()
     expect(second.totalActivityPubMessagesProcessed).to.be.greaterThan(first.totalActivityPubMessagesProcessed)
+
     const apTypes: ActivityType[] = [
       'Create', 'Update', 'Delete', 'Follow', 'Accept', 'Announce', 'Undo', 'Like', 'Reject', 'View', 'Dislike', 'Flag'
     ]
@@ -291,9 +269,7 @@ describe('Test stats (excluding redundancy)', function () {
 
     await wait(6000)
 
-    const res3 = await getStats(servers[1].url)
-    const third: ServerStats = res3.body
-
+    const third = await servers[1].stats.get()
     expect(third.totalActivityPubMessagesWaiting).to.equal(0)
     expect(third.activityPubMessagesProcessedPerSecond).to.be.lessThan(second.activityPubMessagesProcessedPerSecond)
   })
index 4b86e0b904f3f888421d04ddd5d2c66ee445a562..f597ac60ce84504a4857976d78759a033340e050 100644 (file)
@@ -1,36 +1,23 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await,@typescript-eslint/no-floating-promises */
 
-import * as magnetUtil from 'magnet-uri'
 import 'mocha'
-import {
-  cleanupTests,
-  flushAndRunServer,
-  getVideo,
-  killallServers,
-  reRunServer,
-  ServerInfo,
-  uploadVideo
-} from '../../../../shared/extra-utils'
-import { setAccessTokensToServers } from '../../../../shared/extra-utils/index'
-import { VideoDetails } from '../../../../shared/models/videos'
+import * as magnetUtil from 'magnet-uri'
 import * as WebTorrent from 'webtorrent'
+import { cleanupTests, createSingleServer, killallServers, PeerTubeServer, setAccessTokensToServers } from '@shared/extra-utils'
 
 describe('Test tracker', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let badMagnet: string
   let goodMagnet: string
 
   before(async function () {
     this.timeout(60000)
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
     {
-      const res = await uploadVideo(server.url, server.accessToken, {})
-      const videoUUID = res.body.video.uuid
-
-      const resGet = await getVideo(server.url, videoUUID)
-      const video: VideoDetails = resGet.body
+      const { uuid } = await server.videos.upload()
+      const video = await server.videos.get({ id: uuid })
       goodMagnet = video.files[0].magnetUri
 
       const parsed = magnetUtil.decode(goodMagnet)
@@ -61,8 +48,7 @@ describe('Test tracker', function () {
     const errCb = () => done(new Error('Tracker is enabled'))
 
     killallServers([ server ])
-
-    reRunServer(server, { tracker: { enabled: false } })
+      .then(() => server.run({ tracker: { enabled: false } }))
       .then(() => {
         const webtorrent = new WebTorrent()
 
@@ -86,8 +72,7 @@ describe('Test tracker', function () {
     this.timeout(20000)
 
     killallServers([ server ])
-
-    reRunServer(server)
+      .then(() => server.run())
       .then(() => {
         const webtorrent = new WebTorrent()
 
index 60676a37bbb3c00b3bc732eabacc92b6eb459f9b..77b99886d24208a976d63f782ae25c2edd564ea6 100644 (file)
@@ -1,42 +1,30 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import {
   cleanupTests,
-  createUser,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  follow,
-  getVideosList,
-  unfollow,
-  updateVideo,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index'
-import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
-import { Video, VideoChannel } from '../../../../shared/models/videos'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import {
-  addUserSubscription,
-  areSubscriptionsExist,
-  getUserSubscription,
-  listUserSubscriptions,
-  listUserSubscriptionVideos,
-  removeUserSubscription
-} from '../../../../shared/extra-utils/users/user-subscriptions'
+  PeerTubeServer,
+  setAccessTokensToServers,
+  SubscriptionsCommand,
+  waitJobs
+} from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test users subscriptions', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   const users: { accessToken: string }[] = []
   let video3UUID: string
 
+  let command: SubscriptionsCommand
+
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -47,47 +35,50 @@ describe('Test users subscriptions', function () {
     {
       for (const server of servers) {
         const user = { username: 'user' + server.serverNumber, password: 'password' }
-        await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
+        await server.users.create({ username: user.username, password: user.password })
 
-        const accessToken = await userLogin(server, user)
+        const accessToken = await server.login.getAccessToken(user)
         users.push({ accessToken })
 
         const videoName1 = 'video 1-' + server.serverNumber
-        await uploadVideo(server.url, accessToken, { name: videoName1 })
+        await server.videos.upload({ token: accessToken, attributes: { name: videoName1 } })
 
         const videoName2 = 'video 2-' + server.serverNumber
-        await uploadVideo(server.url, accessToken, { name: videoName2 })
+        await server.videos.upload({ token: accessToken, attributes: { name: videoName2 } })
       }
     }
 
     await waitJobs(servers)
+
+    command = servers[0].subscriptions
   })
 
   it('Should display videos of server 2 on server 1', async function () {
-    const res = await getVideosList(servers[0].url)
+    const { total } = await servers[0].videos.list()
 
-    expect(res.body.total).to.equal(4)
+    expect(total).to.equal(4)
   })
 
   it('User of server 1 should follow user of server 3 and root of server 1', async function () {
     this.timeout(60000)
 
-    await addUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:' + servers[2].port)
-    await addUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:' + servers[0].port)
+    await command.add({ token: users[0].accessToken, targetUri: 'user3_channel@localhost:' + servers[2].port })
+    await command.add({ token: users[0].accessToken, targetUri: 'root_channel@localhost:' + servers[0].port })
 
     await waitJobs(servers)
 
-    const res = await uploadVideo(servers[2].url, users[2].accessToken, { name: 'video server 3 added after follow' })
-    video3UUID = res.body.video.uuid
+    const attributes = { name: 'video server 3 added after follow' }
+    const { uuid } = await servers[2].videos.upload({ token: users[2].accessToken, attributes })
+    video3UUID = uuid
 
     await waitJobs(servers)
   })
 
   it('Should not display videos of server 3 on server 1', async function () {
-    const res = await getVideosList(servers[0].url)
+    const { total, data } = await servers[0].videos.list()
+    expect(total).to.equal(4)
 
-    expect(res.body.total).to.equal(4)
-    for (const video of res.body.data) {
+    for (const video of data) {
       expect(video.name).to.not.contain('1-3')
       expect(video.name).to.not.contain('2-3')
       expect(video.name).to.not.contain('video server 3 added after follow')
@@ -96,17 +87,17 @@ describe('Test users subscriptions', function () {
 
   it('Should list subscriptions', async function () {
     {
-      const res = await listUserSubscriptions({ url: servers[0].url, token: servers[0].accessToken })
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      const body = await command.list()
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     }
 
     {
-      const res = await listUserSubscriptions({ url: servers[0].url, token: users[0].accessToken, sort: 'createdAt' })
-      expect(res.body.total).to.equal(2)
+      const body = await command.list({ token: users[0].accessToken, sort: 'createdAt' })
+      expect(body.total).to.equal(2)
 
-      const subscriptions: VideoChannel[] = res.body.data
+      const subscriptions = body.data
       expect(subscriptions).to.be.an('array')
       expect(subscriptions).to.have.lengthOf(2)
 
@@ -117,8 +108,7 @@ describe('Test users subscriptions', function () {
 
   it('Should get subscription', async function () {
     {
-      const res = await getUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:' + servers[2].port)
-      const videoChannel: VideoChannel = res.body
+      const videoChannel = await command.get({ token: users[0].accessToken, uri: 'user3_channel@localhost:' + servers[2].port })
 
       expect(videoChannel.name).to.equal('user3_channel')
       expect(videoChannel.host).to.equal('localhost:' + servers[2].port)
@@ -128,8 +118,7 @@ describe('Test users subscriptions', function () {
     }
 
     {
-      const res = await getUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:' + servers[0].port)
-      const videoChannel: VideoChannel = res.body
+      const videoChannel = await command.get({ token: users[0].accessToken, uri: 'root_channel@localhost:' + servers[0].port })
 
       expect(videoChannel.name).to.equal('root_channel')
       expect(videoChannel.host).to.equal('localhost:' + servers[0].port)
@@ -147,8 +136,7 @@ describe('Test users subscriptions', function () {
       'user3_channel@localhost:' + servers[0].port
     ]
 
-    const res = await areSubscriptionsExist(servers[0].url, users[0].accessToken, uris)
-    const body = res.body
+    const body = await command.exist({ token: users[0].accessToken, uris })
 
     expect(body['user3_channel@localhost:' + servers[2].port]).to.be.true
     expect(body['root2_channel@localhost:' + servers[0].port]).to.be.false
@@ -158,45 +146,31 @@ describe('Test users subscriptions', function () {
 
   it('Should search among subscriptions', async function () {
     {
-      const res = await listUserSubscriptions({
-        url: servers[0].url,
-        token: users[0].accessToken,
-        sort: '-createdAt',
-        search: 'user3_channel'
-      })
-      expect(res.body.total).to.equal(1)
-
-      const subscriptions = res.body.data
-      expect(subscriptions).to.have.lengthOf(1)
+      const body = await command.list({ token: users[0].accessToken, sort: '-createdAt', search: 'user3_channel' })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
     }
 
     {
-      const res = await listUserSubscriptions({
-        url: servers[0].url,
-        token: users[0].accessToken,
-        sort: '-createdAt',
-        search: 'toto'
-      })
-      expect(res.body.total).to.equal(0)
-
-      const subscriptions = res.body.data
-      expect(subscriptions).to.have.lengthOf(0)
+      const body = await command.list({ token: users[0].accessToken, sort: '-createdAt', search: 'toto' })
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
     }
   })
 
   it('Should list subscription videos', async function () {
     {
-      const res = await listUserSubscriptionVideos(servers[0].url, servers[0].accessToken)
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      const body = await command.listVideos()
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     }
 
     {
-      const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
-      expect(res.body.total).to.equal(3)
+      const body = await command.listVideos({ token: users[0].accessToken, sort: 'createdAt' })
+      expect(body.total).to.equal(3)
 
-      const videos: Video[] = res.body.data
+      const videos = body.data
       expect(videos).to.be.an('array')
       expect(videos).to.have.lengthOf(3)
 
@@ -210,22 +184,22 @@ describe('Test users subscriptions', function () {
     this.timeout(60000)
 
     const videoName = 'video server 1 added after follow'
-    await uploadVideo(servers[0].url, servers[0].accessToken, { name: videoName })
+    await servers[0].videos.upload({ attributes: { name: videoName } })
 
     await waitJobs(servers)
 
     {
-      const res = await listUserSubscriptionVideos(servers[0].url, servers[0].accessToken)
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      const body = await command.listVideos()
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     }
 
     {
-      const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
-      expect(res.body.total).to.equal(4)
+      const body = await command.listVideos({ token: users[0].accessToken, sort: 'createdAt' })
+      expect(body.total).to.equal(4)
 
-      const videos: Video[] = res.body.data
+      const videos = body.data
       expect(videos).to.be.an('array')
       expect(videos).to.have.lengthOf(4)
 
@@ -236,10 +210,10 @@ describe('Test users subscriptions', function () {
     }
 
     {
-      const res = await getVideosList(servers[0].url)
+      const { data, total } = await servers[0].videos.list()
+      expect(total).to.equal(5)
 
-      expect(res.body.total).to.equal(5)
-      for (const video of res.body.data) {
+      for (const video of data) {
         expect(video.name).to.not.contain('1-3')
         expect(video.name).to.not.contain('2-3')
         expect(video.name).to.not.contain('video server 3 added after follow')
@@ -250,17 +224,16 @@ describe('Test users subscriptions', function () {
   it('Should have server 1 follow server 3 and display server 3 videos', async function () {
     this.timeout(60000)
 
-    await follow(servers[0].url, [ servers[2].url ], servers[0].accessToken)
+    await servers[0].follows.follow({ hosts: [ servers[2].url ] })
 
     await waitJobs(servers)
 
-    const res = await getVideosList(servers[0].url)
-
-    expect(res.body.total).to.equal(8)
+    const { data, total } = await servers[0].videos.list()
+    expect(total).to.equal(8)
 
     const names = [ '1-3', '2-3', 'video server 3 added after follow' ]
     for (const name of names) {
-      const video = res.body.data.find(v => v.name.indexOf(name) === -1)
+      const video = data.find(v => v.name.includes(name))
       expect(video).to.not.be.undefined
     }
   })
@@ -268,14 +241,14 @@ describe('Test users subscriptions', function () {
   it('Should remove follow server 1 -> server 3 and hide server 3 videos', async function () {
     this.timeout(60000)
 
-    await unfollow(servers[0].url, servers[0].accessToken, servers[2])
+    await servers[0].follows.unfollow({ target: servers[2] })
 
     await waitJobs(servers)
 
-    const res = await getVideosList(servers[0].url)
+    const { total, data } = await servers[0].videos.list()
+    expect(total).to.equal(5)
 
-    expect(res.body.total).to.equal(5)
-    for (const video of res.body.data) {
+    for (const video of data) {
       expect(video.name).to.not.contain('1-3')
       expect(video.name).to.not.contain('2-3')
       expect(video.name).to.not.contain('video server 3 added after follow')
@@ -284,17 +257,17 @@ describe('Test users subscriptions', function () {
 
   it('Should still list subscription videos', async function () {
     {
-      const res = await listUserSubscriptionVideos(servers[0].url, servers[0].accessToken)
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      const body = await command.listVideos()
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     }
 
     {
-      const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
-      expect(res.body.total).to.equal(4)
+      const body = await command.listVideos({ token: users[0].accessToken, sort: 'createdAt' })
+      expect(body.total).to.equal(4)
 
-      const videos: Video[] = res.body.data
+      const videos = body.data
       expect(videos).to.be.an('array')
       expect(videos).to.have.lengthOf(4)
 
@@ -308,58 +281,55 @@ describe('Test users subscriptions', function () {
   it('Should update a video of server 3 and see the updated video on server 1', async function () {
     this.timeout(30000)
 
-    await updateVideo(servers[2].url, users[2].accessToken, video3UUID, { name: 'video server 3 added after follow updated' })
+    await servers[2].videos.update({ id: video3UUID, attributes: { name: 'video server 3 added after follow updated' } })
 
     await waitJobs(servers)
 
-    const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
-    const videos: Video[] = res.body.data
-    expect(videos[2].name).to.equal('video server 3 added after follow updated')
+    const body = await command.listVideos({ token: users[0].accessToken, sort: 'createdAt' })
+    expect(body.data[2].name).to.equal('video server 3 added after follow updated')
   })
 
   it('Should remove user of server 3 subscription', async function () {
     this.timeout(30000)
 
-    await removeUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:' + servers[2].port)
+    await command.remove({ token: users[0].accessToken, uri: 'user3_channel@localhost:' + servers[2].port })
 
     await waitJobs(servers)
   })
 
   it('Should not display its videos anymore', async function () {
-    {
-      const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
-      expect(res.body.total).to.equal(1)
+    const body = await command.listVideos({ token: users[0].accessToken, sort: 'createdAt' })
+    expect(body.total).to.equal(1)
 
-      const videos: Video[] = res.body.data
-      expect(videos).to.be.an('array')
-      expect(videos).to.have.lengthOf(1)
+    const videos = body.data
+    expect(videos).to.be.an('array')
+    expect(videos).to.have.lengthOf(1)
 
-      expect(videos[0].name).to.equal('video server 1 added after follow')
-    }
+    expect(videos[0].name).to.equal('video server 1 added after follow')
   })
 
   it('Should remove the root subscription and not display the videos anymore', async function () {
     this.timeout(30000)
 
-    await removeUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:' + servers[0].port)
+    await command.remove({ token: users[0].accessToken, uri: 'root_channel@localhost:' + servers[0].port })
 
     await waitJobs(servers)
 
     {
-      const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
-      expect(res.body.total).to.equal(0)
+      const body = await command.list({ token: users[0].accessToken, sort: 'createdAt' })
+      expect(body.total).to.equal(0)
 
-      const videos: Video[] = res.body.data
+      const videos = body.data
       expect(videos).to.be.an('array')
       expect(videos).to.have.lengthOf(0)
     }
   })
 
   it('Should correctly display public videos on server 1', async function () {
-    const res = await getVideosList(servers[0].url)
+    const { total, data } = await servers[0].videos.list()
+    expect(total).to.equal(5)
 
-    expect(res.body.total).to.equal(5)
-    for (const video of res.body.data) {
+    for (const video of data) {
       expect(video.name).to.not.contain('1-3')
       expect(video.name).to.not.contain('2-3')
       expect(video.name).to.not.contain('video server 3 added after follow updated')
@@ -369,15 +339,15 @@ describe('Test users subscriptions', function () {
   it('Should follow user of server 3 again', async function () {
     this.timeout(60000)
 
-    await addUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:' + servers[2].port)
+    await command.add({ token: users[0].accessToken, targetUri: 'user3_channel@localhost:' + servers[2].port })
 
     await waitJobs(servers)
 
     {
-      const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
-      expect(res.body.total).to.equal(3)
+      const body = await command.listVideos({ token: users[0].accessToken, sort: 'createdAt' })
+      expect(body.total).to.equal(3)
 
-      const videos: Video[] = res.body.data
+      const videos = body.data
       expect(videos).to.be.an('array')
       expect(videos).to.have.lengthOf(3)
 
@@ -387,10 +357,10 @@ describe('Test users subscriptions', function () {
     }
 
     {
-      const res = await getVideosList(servers[0].url)
+      const { total, data } = await servers[0].videos.list()
+      expect(total).to.equal(5)
 
-      expect(res.body.total).to.equal(5)
-      for (const video of res.body.data) {
+      for (const video of data) {
         expect(video.name).to.not.contain('1-3')
         expect(video.name).to.not.contain('2-3')
         expect(video.name).to.not.contain('video server 3 added after follow updated')
index f60c66e4bd26ec01d8b0d03b8415bf923cbdbabb..d0ca82b07867f77371d406c913128892361bc159 100644 (file)
@@ -1,34 +1,30 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { Account } from '../../../../shared/models/actors'
+import * as chai from 'chai'
 import {
+  checkActorFilesWereRemoved,
   checkTmpIsEmpty,
   checkVideoFilesWereRemoved,
   cleanupTests,
-  createUser,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getAccountVideos,
-  getVideoChannelsList,
-  removeUser,
-  updateMyUser,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { getMyUserInformation, ServerInfo, testImage, updateMyAvatar, uploadVideo } from '../../../../shared/extra-utils/index'
-import { checkActorFilesWereRemoved, getAccount, getAccountsList } from '../../../../shared/extra-utils/users/accounts'
-import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
-import { User } from '../../../../shared/models/users'
-import { VideoChannel } from '../../../../shared/models/videos'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
+  PeerTubeServer,
+  saveVideoInServers,
+  setAccessTokensToServers,
+  testImage,
+  waitJobs
+} from '@shared/extra-utils'
+import { MyUser } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test users with multiple servers', function () {
-  let servers: ServerInfo[] = []
-  let user: User
+  let servers: PeerTubeServer[] = []
+
+  let user: MyUser
   let userId: number
+
   let videoUUID: string
   let userAccessToken: string
   let userAvatarFilename: string
@@ -36,7 +32,7 @@ describe('Test users with multiple servers', function () {
   before(async function () {
     this.timeout(120_000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -49,43 +45,31 @@ describe('Test users with multiple servers', function () {
     await doubleFollow(servers[1], servers[2])
 
     // The root user of server 1 is propagated to servers 2 and 3
-    await uploadVideo(servers[0].url, servers[0].accessToken, {})
+    await servers[0].videos.upload()
 
     {
-      const user = {
-        username: 'user1',
-        password: 'password'
-      }
-      const res = await createUser({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
-        username: user.username,
-        password: user.password
-      })
-      userId = res.body.user.id
-      userAccessToken = await userLogin(servers[0], user)
+      const username = 'user1'
+      const created = await servers[0].users.create({ username })
+      userId = created.id
+      userAccessToken = await servers[0].login.getAccessToken(username)
     }
 
     {
-      const resVideo = await uploadVideo(servers[0].url, userAccessToken, {})
-      videoUUID = resVideo.body.video.uuid
-    }
+      const { uuid } = await servers[0].videos.upload({ token: userAccessToken })
+      videoUUID = uuid
 
-    await waitJobs(servers)
+      await waitJobs(servers)
+
+      await saveVideoInServers(servers, videoUUID)
+    }
   })
 
   it('Should be able to update my display name', async function () {
     this.timeout(10000)
 
-    await updateMyUser({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      displayName: 'my super display name'
-    })
-
-    const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
-    user = res.body
+    await servers[0].users.updateMe({ displayName: 'my super display name' })
 
+    user = await servers[0].users.getMyInfo()
     expect(user.account.displayName).to.equal('my super display name')
 
     await waitJobs(servers)
@@ -94,14 +78,9 @@ describe('Test users with multiple servers', function () {
   it('Should be able to update my description', async function () {
     this.timeout(10_000)
 
-    await updateMyUser({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      description: 'my super description updated'
-    })
+    await servers[0].users.updateMe({ description: 'my super description updated' })
 
-    const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
-    user = res.body
+    user = await servers[0].users.getMyInfo()
     expect(user.account.displayName).to.equal('my super display name')
     expect(user.account.description).to.equal('my super description updated')
 
@@ -113,15 +92,9 @@ describe('Test users with multiple servers', function () {
 
     const fixture = 'avatar2.png'
 
-    await updateMyAvatar({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      fixture
-    })
-
-    const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
-    user = res.body
+    await servers[0].users.updateMyAvatar({ fixture })
 
+    user = await servers[0].users.getMyInfo()
     userAvatarFilename = user.account.avatar.path
 
     await testImage(servers[0].url, 'avatar2-resized', userAvatarFilename, '.png')
@@ -133,13 +106,12 @@ describe('Test users with multiple servers', function () {
     let createdAt: string | Date
 
     for (const server of servers) {
-      const resAccounts = await getAccountsList(server.url, '-createdAt')
+      const body = await server.accounts.list({ sort: '-createdAt' })
 
-      const resList = resAccounts.body.data.find(a => a.name === 'root' && a.host === 'localhost:' + servers[0].port) as Account
+      const resList = body.data.find(a => a.name === 'root' && a.host === 'localhost:' + servers[0].port)
       expect(resList).not.to.be.undefined
 
-      const resAccount = await getAccount(server.url, resList.name + '@' + resList.host)
-      const account = resAccount.body as Account
+      const account = await server.accounts.get({ accountName: resList.name + '@' + resList.host })
 
       if (!createdAt) createdAt = account.createdAt
 
@@ -161,31 +133,29 @@ describe('Test users with multiple servers', function () {
 
   it('Should list account videos', async function () {
     for (const server of servers) {
-      const res = await getAccountVideos(server.url, server.accessToken, 'user1@localhost:' + servers[0].port, 0, 5)
+      const { total, data } = await server.videos.listByAccount({ handle: 'user1@localhost:' + servers[0].port })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].uuid).to.equal(videoUUID)
+      expect(total).to.equal(1)
+      expect(data).to.be.an('array')
+      expect(data).to.have.lengthOf(1)
+      expect(data[0].uuid).to.equal(videoUUID)
     }
   })
 
   it('Should search through account videos', async function () {
     this.timeout(10_000)
 
-    const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'Kami no chikara' })
+    const created = await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'Kami no chikara' } })
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getAccountVideos(server.url, server.accessToken, 'user1@localhost:' + servers[0].port, 0, 5, undefined, {
-        search: 'Kami'
-      })
-
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].uuid).to.equal(resVideo.body.video.uuid)
+      const { total, data } = await server.videos.listByAccount({ handle: 'user1@localhost:' + servers[0].port, search: 'Kami' })
+
+      expect(total).to.equal(1)
+      expect(data).to.be.an('array')
+      expect(data).to.have.lengthOf(1)
+      expect(data[0].uuid).to.equal(created.uuid)
     }
   })
 
@@ -193,32 +163,28 @@ describe('Test users with multiple servers', function () {
     this.timeout(10_000)
 
     for (const server of servers) {
-      const resAccounts = await getAccountsList(server.url, '-createdAt')
+      const body = await server.accounts.list({ sort: '-createdAt' })
 
-      const accountDeleted = resAccounts.body.data.find(a => a.name === 'user1' && a.host === 'localhost:' + servers[0].port) as Account
+      const accountDeleted = body.data.find(a => a.name === 'user1' && a.host === 'localhost:' + servers[0].port)
       expect(accountDeleted).not.to.be.undefined
 
-      const resVideoChannels = await getVideoChannelsList(server.url, 0, 10)
-      const videoChannelDeleted = resVideoChannels.body.data.find(a => {
-        return a.displayName === 'Main user1 channel' && a.host === 'localhost:' + servers[0].port
-      }) as VideoChannel
+      const { data } = await server.channels.list()
+      const videoChannelDeleted = data.find(a => a.displayName === 'Main user1 channel' && a.host === 'localhost:' + servers[0].port)
       expect(videoChannelDeleted).not.to.be.undefined
     }
 
-    await removeUser(servers[0].url, userId, servers[0].accessToken)
+    await servers[0].users.remove({ userId })
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const resAccounts = await getAccountsList(server.url, '-createdAt')
+      const body = await server.accounts.list({ sort: '-createdAt' })
 
-      const accountDeleted = resAccounts.body.data.find(a => a.name === 'user1' && a.host === 'localhost:' + servers[0].port) as Account
+      const accountDeleted = body.data.find(a => a.name === 'user1' && a.host === 'localhost:' + servers[0].port)
       expect(accountDeleted).to.be.undefined
 
-      const resVideoChannels = await getVideoChannelsList(server.url, 0, 10)
-      const videoChannelDeleted = resVideoChannels.body.data.find(a => {
-        return a.name === 'Main user1 channel' && a.host === 'localhost:' + servers[0].port
-      }) as VideoChannel
+      const { data } = await server.channels.list()
+      const videoChannelDeleted = data.find(a => a.name === 'Main user1 channel' && a.host === 'localhost:' + servers[0].port)
       expect(videoChannelDeleted).to.be.undefined
     }
   })
@@ -231,7 +197,7 @@ describe('Test users with multiple servers', function () {
 
   it('Should not have video files', async () => {
     for (const server of servers) {
-      await checkVideoFilesWereRemoved(videoUUID, server.internalServerNumber)
+      await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
     }
   })
 
index e0f2f211286776e991ea9052a8176edd6b8605ad..f54463359da6b6b667f1ad5fb92d8087d7d2fbde 100644 (file)
@@ -1,30 +1,14 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import {
-  cleanupTests,
-  flushAndRunServer,
-  getMyUserInformation,
-  getUserInformation,
-  login,
-  registerUser,
-  ServerInfo,
-  updateCustomSubConfig,
-  updateMyUser,
-  userLogin,
-  verifyEmail
-} from '../../../../shared/extra-utils'
-import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
-import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { User } from '../../../../shared/models/users'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import * as chai from 'chai'
+import { cleanupTests, createSingleServer, MockSmtpServer, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test users account verification', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userId: number
   let userAccessToken: string
   let verificationString: string
@@ -50,7 +34,7 @@ describe('Test users account verification', function () {
         port
       }
     }
-    server = await flushAndRunServer(1, overrideConfig)
+    server = await createSingleServer(1, overrideConfig)
 
     await setAccessTokensToServers([ server ])
   })
@@ -58,15 +42,17 @@ describe('Test users account verification', function () {
   it('Should register user and send verification email if verification required', async function () {
     this.timeout(30000)
 
-    await updateCustomSubConfig(server.url, server.accessToken, {
-      signup: {
-        enabled: true,
-        requiresEmailVerification: true,
-        limit: 10
+    await server.config.updateCustomSubConfig({
+      newConfig: {
+        signup: {
+          enabled: true,
+          requiresEmailVerification: true,
+          limit: 10
+        }
       }
     })
 
-    await registerUser(server.url, user1.username, user1.password)
+    await server.users.register(user1)
 
     await waitJobs(server)
     expectedEmailsLength++
@@ -85,23 +71,23 @@ describe('Test users account verification', function () {
 
     userId = parseInt(userIdMatches[1], 10)
 
-    const resUserInfo = await getUserInformation(server.url, server.accessToken, userId)
-    expect(resUserInfo.body.emailVerified).to.be.false
+    const body = await server.users.get({ userId })
+    expect(body.emailVerified).to.be.false
   })
 
   it('Should not allow login for user with unverified email', async function () {
-    const resLogin = await login(server.url, server.client, user1, HttpStatusCode.BAD_REQUEST_400)
-    expect(resLogin.body.detail).to.contain('User email is not verified.')
+    const { detail } = await server.login.login({ user: user1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    expect(detail).to.contain('User email is not verified.')
   })
 
   it('Should verify the user via email and allow login', async function () {
-    await verifyEmail(server.url, userId, verificationString)
+    await server.users.verifyEmail({ userId, verificationString })
 
-    const res = await login(server.url, server.client, user1)
-    userAccessToken = res.body.access_token
+    const body = await server.login.login({ user: user1 })
+    userAccessToken = body.access_token
 
-    const resUserVerified = await getUserInformation(server.url, server.accessToken, userId)
-    expect(resUserVerified.body.emailVerified).to.be.true
+    const user = await server.users.get({ userId })
+    expect(user.emailVerified).to.be.true
   })
 
   it('Should be able to change the user email', async function () {
@@ -110,9 +96,8 @@ describe('Test users account verification', function () {
     let updateVerificationString: string
 
     {
-      await updateMyUser({
-        url: server.url,
-        accessToken: userAccessToken,
+      await server.users.updateMe({
+        token: userAccessToken,
         email: 'updated@example.com',
         currentPassword: user1.password
       })
@@ -128,19 +113,15 @@ describe('Test users account verification', function () {
     }
 
     {
-      const res = await getMyUserInformation(server.url, userAccessToken)
-      const me: User = res.body
-
+      const me = await server.users.getMyInfo({ token: userAccessToken })
       expect(me.email).to.equal('user_1@example.com')
       expect(me.pendingEmail).to.equal('updated@example.com')
     }
 
     {
-      await verifyEmail(server.url, userId, updateVerificationString, true)
-
-      const res = await getMyUserInformation(server.url, userAccessToken)
-      const me: User = res.body
+      await server.users.verifyEmail({ userId, verificationString: updateVerificationString, isPendingEmail: true })
 
+      const me = await server.users.getMyInfo({ token: userAccessToken })
       expect(me.email).to.equal('updated@example.com')
       expect(me.pendingEmail).to.be.null
     }
@@ -148,35 +129,39 @@ describe('Test users account verification', function () {
 
   it('Should register user not requiring email verification if setting not enabled', async function () {
     this.timeout(5000)
-    await updateCustomSubConfig(server.url, server.accessToken, {
-      signup: {
-        enabled: true,
-        requiresEmailVerification: false,
-        limit: 10
+    await server.config.updateCustomSubConfig({
+      newConfig: {
+        signup: {
+          enabled: true,
+          requiresEmailVerification: false,
+          limit: 10
+        }
       }
     })
 
-    await registerUser(server.url, user2.username, user2.password)
+    await server.users.register(user2)
 
     await waitJobs(server)
     expect(emails).to.have.lengthOf(expectedEmailsLength)
 
-    const accessToken = await userLogin(server, user2)
+    const accessToken = await server.login.getAccessToken(user2)
 
-    const resMyUserInfo = await getMyUserInformation(server.url, accessToken)
-    expect(resMyUserInfo.body.emailVerified).to.be.null
+    const user = await server.users.getMyInfo({ token: accessToken })
+    expect(user.emailVerified).to.be.null
   })
 
   it('Should allow login for user with unverified email when setting later enabled', async function () {
-    await updateCustomSubConfig(server.url, server.accessToken, {
-      signup: {
-        enabled: true,
-        requiresEmailVerification: true,
-        limit: 10
+    await server.config.updateCustomSubConfig({
+      newConfig: {
+        signup: {
+          enabled: true,
+          requiresEmailVerification: true,
+          limit: 10
+        }
       }
     })
 
-    await userLogin(server, user2)
+    await server.login.getAccessToken(user2)
   })
 
   after(async function () {
index 87ba775f650b3984ae13a0d748c5c9f4473512d0..1419ae820493e479124c09784cf7bf3340eaf9af 100644 (file)
@@ -2,63 +2,24 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models'
-import { CustomConfig, OAuth2ErrorCode } from '@shared/models/server'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
-  addVideoCommentThread,
-  blockUser,
   cleanupTests,
-  closeAllSequelize,
-  createUser,
-  deleteMe,
-  flushAndRunServer,
-  getAccountRatings,
-  getAdminAbusesList,
-  getBlacklistedVideosList,
-  getCustomConfig,
-  getMyUserInformation,
-  getMyUserVideoQuotaUsed,
-  getMyUserVideoRating,
-  getUserInformation,
-  getUsersList,
-  getUsersListPaginationAndSort,
-  getVideoChannel,
-  getVideosList,
-  installPlugin,
+  createSingleServer,
   killallServers,
-  login,
   makePutBodyRequest,
-  rateVideo,
-  registerUserWithChannel,
-  removeUser,
-  removeVideo,
-  reportAbuse,
-  reRunServer,
-  ServerInfo,
-  setTokenField,
+  PeerTubeServer,
+  setAccessTokensToServers,
   testImage,
-  unblockUser,
-  updateAbuse,
-  updateCustomSubConfig,
-  updateMyAvatar,
-  updateMyUser,
-  updateUser,
-  uploadVideo,
-  userLogin,
   waitJobs
-} from '../../../../shared/extra-utils'
-import { follow } from '../../../../shared/extra-utils/server/follows'
-import { logout, refreshToken, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
-import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
-import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
+} from '@shared/extra-utils'
+import { AbuseState, HttpStatusCode, OAuth2ErrorCode, UserAdminFlag, UserRole, Video, VideoPlaylistType } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test users', function () {
-  let server: ServerInfo
-  let accessToken: string
-  let accessTokenUser: string
+  let server: PeerTubeServer
+  let token: string
+  let userToken: string
   let videoId: number
   let userId: number
   const user = {
@@ -69,7 +30,7 @@ describe('Test users', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1, {
+    server = await createSingleServer(1, {
       rates_limit: {
         login: {
           max: 30
@@ -79,7 +40,7 @@ describe('Test users', function () {
 
     await setAccessTokensToServers([ server ])
 
-    await installPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-theme-background-red' })
+    await server.plugins.install({ npmName: 'peertube-theme-background-red' })
   })
 
   describe('OAuth client', function () {
@@ -90,158 +51,156 @@ describe('Test users', function () {
     it('Should remove the last client')
 
     it('Should not login with an invalid client id', async function () {
-      const client = { id: 'client', secret: server.client.secret }
-      const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
+      const client = { id: 'client', secret: server.store.client.secret }
+      const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
 
-      expect(res.body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT)
-      expect(res.body.error).to.contain('client is invalid')
-      expect(res.body.type.startsWith('https://')).to.be.true
-      expect(res.body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT)
+      expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT)
+      expect(body.error).to.contain('client is invalid')
+      expect(body.type.startsWith('https://')).to.be.true
+      expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT)
     })
 
     it('Should not login with an invalid client secret', async function () {
-      const client = { id: server.client.id, secret: 'coucou' }
-      const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
+      const client = { id: server.store.client.id, secret: 'coucou' }
+      const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
 
-      expect(res.body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT)
-      expect(res.body.error).to.contain('client is invalid')
-      expect(res.body.type.startsWith('https://')).to.be.true
-      expect(res.body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT)
+      expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT)
+      expect(body.error).to.contain('client is invalid')
+      expect(body.type.startsWith('https://')).to.be.true
+      expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT)
     })
   })
 
   describe('Login', function () {
 
     it('Should not login with an invalid username', async function () {
-      const user = { username: 'captain crochet', password: server.user.password }
-      const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
+      const user = { username: 'captain crochet', password: server.store.user.password }
+      const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
 
-      expect(res.body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT)
-      expect(res.body.error).to.contain('credentials are invalid')
-      expect(res.body.type.startsWith('https://')).to.be.true
-      expect(res.body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT)
+      expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT)
+      expect(body.error).to.contain('credentials are invalid')
+      expect(body.type.startsWith('https://')).to.be.true
+      expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT)
     })
 
     it('Should not login with an invalid password', async function () {
-      const user = { username: server.user.username, password: 'mew_three' }
-      const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
+      const user = { username: server.store.user.username, password: 'mew_three' }
+      const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
 
-      expect(res.body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT)
-      expect(res.body.error).to.contain('credentials are invalid')
-      expect(res.body.type.startsWith('https://')).to.be.true
-      expect(res.body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT)
+      expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT)
+      expect(body.error).to.contain('credentials are invalid')
+      expect(body.type.startsWith('https://')).to.be.true
+      expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT)
     })
 
     it('Should not be able to upload a video', async function () {
-      accessToken = 'my_super_token'
+      token = 'my_super_token'
 
-      const videoAttributes = {}
-      await uploadVideo(server.url, accessToken, videoAttributes, HttpStatusCode.UNAUTHORIZED_401)
+      await server.videos.upload({ token, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should not be able to follow', async function () {
-      accessToken = 'my_super_token'
-      await follow(server.url, [ 'http://example.com' ], accessToken, HttpStatusCode.UNAUTHORIZED_401)
+      token = 'my_super_token'
+
+      await server.follows.follow({
+        hosts: [ 'http://example.com' ],
+        token,
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
+      })
     })
 
     it('Should not be able to unfollow')
 
     it('Should be able to login', async function () {
-      const res = await login(server.url, server.client, server.user, HttpStatusCode.OK_200)
+      const body = await server.login.login({ expectedStatus: HttpStatusCode.OK_200 })
 
-      accessToken = res.body.access_token
+      token = body.access_token
     })
 
     it('Should be able to login with an insensitive username', async function () {
-      const user = { username: 'RoOt', password: server.user.password }
-      await login(server.url, server.client, user, HttpStatusCode.OK_200)
+      const user = { username: 'RoOt', password: server.store.user.password }
+      await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 })
 
-      const user2 = { username: 'rOoT', password: server.user.password }
-      await login(server.url, server.client, user2, HttpStatusCode.OK_200)
+      const user2 = { username: 'rOoT', password: server.store.user.password }
+      await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 })
 
-      const user3 = { username: 'ROOt', password: server.user.password }
-      await login(server.url, server.client, user3, HttpStatusCode.OK_200)
+      const user3 = { username: 'ROOt', password: server.store.user.password }
+      await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
   describe('Upload', function () {
 
     it('Should upload the video with the correct token', async function () {
-      const videoAttributes = {}
-      await uploadVideo(server.url, accessToken, videoAttributes)
-      const res = await getVideosList(server.url)
-      const video = res.body.data[0]
+      await server.videos.upload({ token })
+      const { data } = await server.videos.list()
+      const video = data[0]
 
       expect(video.account.name).to.equal('root')
       videoId = video.id
     })
 
     it('Should upload the video again with the correct token', async function () {
-      const videoAttributes = {}
-      await uploadVideo(server.url, accessToken, videoAttributes)
+      await server.videos.upload({ token })
     })
   })
 
   describe('Ratings', function () {
 
     it('Should retrieve a video rating', async function () {
-      await rateVideo(server.url, accessToken, videoId, 'like')
-      const res = await getMyUserVideoRating(server.url, accessToken, videoId)
-      const rating = res.body
+      await server.videos.rate({ id: videoId, rating: 'like' })
+      const rating = await server.users.getMyRating({ token, videoId })
 
       expect(rating.videoId).to.equal(videoId)
       expect(rating.rating).to.equal('like')
     })
 
     it('Should retrieve ratings list', async function () {
-      await rateVideo(server.url, accessToken, videoId, 'like')
+      await server.videos.rate({ id: videoId, rating: 'like' })
 
-      const res = await getAccountRatings(server.url, server.user.username, server.accessToken, null, HttpStatusCode.OK_200)
-      const ratings = res.body
+      const body = await server.accounts.listRatings({ accountName: server.store.user.username })
 
-      expect(ratings.total).to.equal(1)
-      expect(ratings.data[0].video.id).to.equal(videoId)
-      expect(ratings.data[0].rating).to.equal('like')
+      expect(body.total).to.equal(1)
+      expect(body.data[0].video.id).to.equal(videoId)
+      expect(body.data[0].rating).to.equal('like')
     })
 
     it('Should retrieve ratings list by rating type', async function () {
       {
-        const res = await getAccountRatings(server.url, server.user.username, server.accessToken, 'like')
-        const ratings = res.body
-        expect(ratings.data.length).to.equal(1)
+        const body = await server.accounts.listRatings({ accountName: server.store.user.username, rating: 'like' })
+        expect(body.data.length).to.equal(1)
       }
 
       {
-        const res = await getAccountRatings(server.url, server.user.username, server.accessToken, 'dislike')
-        const ratings = res.body
-        expect(ratings.data.length).to.equal(0)
+        const body = await server.accounts.listRatings({ accountName: server.store.user.username, rating: 'dislike' })
+        expect(body.data.length).to.equal(0)
       }
     })
   })
 
   describe('Remove video', function () {
     it('Should not be able to remove the video with an incorrect token', async function () {
-      await removeVideo(server.url, 'bad_token', videoId, HttpStatusCode.UNAUTHORIZED_401)
+      await server.videos.remove({ token: 'bad_token', id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should not be able to remove the video with the token of another account')
 
     it('Should be able to remove the video with the correct token', async function () {
-      await removeVideo(server.url, accessToken, videoId)
+      await server.videos.remove({ token, id: videoId })
     })
   })
 
   describe('Logout', function () {
     it('Should logout (revoke token)', async function () {
-      await logout(server.url, server.accessToken)
+      await server.login.logout({ token: server.accessToken })
     })
 
     it('Should not be able to get the user information', async function () {
-      await getMyUserInformation(server.url, server.accessToken, HttpStatusCode.UNAUTHORIZED_401)
+      await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should not be able to upload a video', async function () {
-      await uploadVideo(server.url, server.accessToken, { name: 'video' }, HttpStatusCode.UNAUTHORIZED_401)
+      await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should not be able to rate a video', async function () {
@@ -255,79 +214,70 @@ describe('Test users', function () {
         path: path + videoId,
         token: 'wrong token',
         fields: data,
-        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
       }
       await makePutBodyRequest(options)
     })
 
     it('Should be able to login again', async function () {
-      const res = await login(server.url, server.client, server.user)
-      server.accessToken = res.body.access_token
-      server.refreshToken = res.body.refresh_token
+      const body = await server.login.login()
+      server.accessToken = body.access_token
+      server.refreshToken = body.refresh_token
     })
 
     it('Should be able to get my user information again', async function () {
-      await getMyUserInformation(server.url, server.accessToken)
+      await server.users.getMyInfo()
     })
 
     it('Should have an expired access token', async function () {
       this.timeout(15000)
 
-      await setTokenField(server.internalServerNumber, server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
-      await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
+      await server.sql.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
+      await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
 
-      killallServers([ server ])
-      await reRunServer(server)
+      await killallServers([ server ])
+      await server.run()
 
-      await getMyUserInformation(server.url, server.accessToken, 401)
+      await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should not be able to refresh an access token with an expired refresh token', async function () {
-      await refreshToken(server, server.refreshToken, 400)
+      await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should refresh the token', async function () {
       this.timeout(15000)
 
       const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString()
-      await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', futureDate)
+      await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate)
 
-      killallServers([ server ])
-      await reRunServer(server)
+      await killallServers([ server ])
+      await server.run()
 
-      const res = await refreshToken(server, server.refreshToken)
+      const res = await server.login.refreshToken({ refreshToken: server.refreshToken })
       server.accessToken = res.body.access_token
       server.refreshToken = res.body.refresh_token
     })
 
     it('Should be able to get my user information again', async function () {
-      await getMyUserInformation(server.url, server.accessToken)
+      await server.users.getMyInfo()
     })
   })
 
   describe('Creating a user', function () {
 
     it('Should be able to create a new user', async function () {
-      await createUser({
-        url: server.url,
-        accessToken: accessToken,
-        username: user.username,
-        password: user.password,
-        videoQuota: 2 * 1024 * 1024,
-        adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST
-      })
+      await server.users.create({ ...user, videoQuota: 2 * 1024 * 1024, adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST })
     })
 
     it('Should be able to login with this user', async function () {
-      accessTokenUser = await userLogin(server, user)
+      userToken = await server.login.getAccessToken(user)
     })
 
     it('Should be able to get user information', async function () {
-      const res1 = await getMyUserInformation(server.url, accessTokenUser)
-      const userMe: MyUser = res1.body
+      const userMe = await server.users.getMyInfo({ token: userToken })
 
-      const res2 = await getUserInformation(server.url, server.accessToken, userMe.id, true)
-      const userGet: User = res2.body
+      const userGet = await server.users.get({ userId: userMe.id, withStats: true })
 
       for (const user of [ userMe, userGet ]) {
         expect(user.username).to.equal('user_1')
@@ -363,34 +313,28 @@ describe('Test users', function () {
     it('Should be able to upload a video with this user', async function () {
       this.timeout(10000)
 
-      const videoAttributes = {
+      const attributes = {
         name: 'super user video',
         fixture: 'video_short.webm'
       }
-      await uploadVideo(server.url, accessTokenUser, videoAttributes)
+      await server.videos.upload({ token: userToken, attributes })
     })
 
     it('Should have video quota updated', async function () {
-      const res = await getMyUserVideoQuotaUsed(server.url, accessTokenUser)
-      const data = res.body
-
-      expect(data.videoQuotaUsed).to.equal(218910)
-
-      const resUsers = await getUsersList(server.url, server.accessToken)
+      const quota = await server.users.getMyQuotaUsed({ token: userToken })
+      expect(quota.videoQuotaUsed).to.equal(218910)
 
-      const users: User[] = resUsers.body.data
-      const tmpUser = users.find(u => u.username === user.username)
+      const { data } = await server.users.list()
+      const tmpUser = data.find(u => u.username === user.username)
       expect(tmpUser.videoQuotaUsed).to.equal(218910)
     })
 
     it('Should be able to list my videos', async function () {
-      const res = await getMyVideos(server.url, accessTokenUser, 0, 5)
-      expect(res.body.total).to.equal(1)
+      const { total, data } = await server.videos.listMyVideos({ token: userToken })
+      expect(total).to.equal(1)
+      expect(data).to.have.lengthOf(1)
 
-      const videos = res.body.data
-      expect(videos).to.have.lengthOf(1)
-
-      const video: Video = videos[0]
+      const video: Video = data[0]
       expect(video.name).to.equal('super user video')
       expect(video.thumbnailPath).to.not.be.null
       expect(video.previewPath).to.not.be.null
@@ -398,19 +342,15 @@ describe('Test users', function () {
 
     it('Should be able to search in my videos', async function () {
       {
-        const res = await getMyVideos(server.url, accessTokenUser, 0, 5, '-createdAt', 'user video')
-        expect(res.body.total).to.equal(1)
-
-        const videos = res.body.data
-        expect(videos).to.have.lengthOf(1)
+        const { total, data } = await server.videos.listMyVideos({ token: userToken, sort: '-createdAt', search: 'user video' })
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
       }
 
       {
-        const res = await getMyVideos(server.url, accessTokenUser, 0, 5, '-createdAt', 'toto')
-        expect(res.body.total).to.equal(0)
-
-        const videos = res.body.data
-        expect(videos).to.have.lengthOf(0)
+        const { total, data } = await server.videos.listMyVideos({ token: userToken, sort: '-createdAt', search: 'toto' })
+        expect(total).to.equal(0)
+        expect(data).to.have.lengthOf(0)
       }
     })
 
@@ -418,28 +358,25 @@ describe('Test users', function () {
       this.timeout(60000)
 
       {
-        const res = await getCustomConfig(server.url, server.accessToken)
-        const config = res.body as CustomConfig
+        const config = await server.config.getCustomConfig()
         config.transcoding.webtorrent.enabled = false
         config.transcoding.hls.enabled = true
         config.transcoding.enabled = true
-        await updateCustomSubConfig(server.url, server.accessToken, config)
+        await server.config.updateCustomSubConfig({ newConfig: config })
       }
 
       {
-        const videoAttributes = {
+        const attributes = {
           name: 'super user video 2',
           fixture: 'video_short.webm'
         }
-        await uploadVideo(server.url, accessTokenUser, videoAttributes)
+        await server.videos.upload({ token: userToken, attributes })
 
         await waitJobs([ server ])
       }
 
       {
-        const res = await getMyUserVideoQuotaUsed(server.url, accessTokenUser)
-        const data = res.body
-
+        const data = await server.users.getMyQuotaUsed({ token: userToken })
         expect(data.videoQuotaUsed).to.be.greaterThan(220000)
       }
     })
@@ -448,21 +385,18 @@ describe('Test users', function () {
   describe('Users listing', function () {
 
     it('Should list all the users', async function () {
-      const res = await getUsersList(server.url, server.accessToken)
-      const result = res.body
-      const total = result.total
-      const users = result.data
+      const { data, total } = await server.users.list()
 
       expect(total).to.equal(2)
-      expect(users).to.be.an('array')
-      expect(users.length).to.equal(2)
+      expect(data).to.be.an('array')
+      expect(data.length).to.equal(2)
 
-      const user = users[0]
+      const user = data[0]
       expect(user.username).to.equal('user_1')
       expect(user.email).to.equal('user_1@example.com')
       expect(user.nsfwPolicy).to.equal('display')
 
-      const rootUser = users[1]
+      const rootUser = data[1]
       expect(rootUser.username).to.equal('root')
       expect(rootUser.email).to.equal('admin' + server.internalServerNumber + '@example.com')
       expect(user.nsfwPolicy).to.equal('display')
@@ -474,16 +408,12 @@ describe('Test users', function () {
     })
 
     it('Should list only the first user by username asc', async function () {
-      const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 1, 'username')
-
-      const result = res.body
-      const total = result.total
-      const users = result.data
+      const { total, data } = await server.users.list({ start: 0, count: 1, sort: 'username' })
 
       expect(total).to.equal(2)
-      expect(users.length).to.equal(1)
+      expect(data.length).to.equal(1)
 
-      const user = users[0]
+      const user = data[0]
       expect(user.username).to.equal('root')
       expect(user.email).to.equal('admin' + server.internalServerNumber + '@example.com')
       expect(user.roleLabel).to.equal('Administrator')
@@ -491,111 +421,90 @@ describe('Test users', function () {
     })
 
     it('Should list only the first user by username desc', async function () {
-      const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 1, '-username')
-      const result = res.body
-      const total = result.total
-      const users = result.data
+      const { total, data } = await server.users.list({ start: 0, count: 1, sort: '-username' })
 
       expect(total).to.equal(2)
-      expect(users.length).to.equal(1)
+      expect(data.length).to.equal(1)
 
-      const user = users[0]
+      const user = data[0]
       expect(user.username).to.equal('user_1')
       expect(user.email).to.equal('user_1@example.com')
       expect(user.nsfwPolicy).to.equal('display')
     })
 
     it('Should list only the second user by createdAt desc', async function () {
-      const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 1, '-createdAt')
-      const result = res.body
-      const total = result.total
-      const users = result.data
-
+      const { data, total } = await server.users.list({ start: 0, count: 1, sort: '-createdAt' })
       expect(total).to.equal(2)
-      expect(users.length).to.equal(1)
 
-      const user = users[0]
+      expect(data.length).to.equal(1)
+
+      const user = data[0]
       expect(user.username).to.equal('user_1')
       expect(user.email).to.equal('user_1@example.com')
       expect(user.nsfwPolicy).to.equal('display')
     })
 
     it('Should list all the users by createdAt asc', async function () {
-      const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt')
-      const result = res.body
-      const total = result.total
-      const users = result.data
+      const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt' })
 
       expect(total).to.equal(2)
-      expect(users.length).to.equal(2)
+      expect(data.length).to.equal(2)
 
-      expect(users[0].username).to.equal('root')
-      expect(users[0].email).to.equal('admin' + server.internalServerNumber + '@example.com')
-      expect(users[0].nsfwPolicy).to.equal('display')
+      expect(data[0].username).to.equal('root')
+      expect(data[0].email).to.equal('admin' + server.internalServerNumber + '@example.com')
+      expect(data[0].nsfwPolicy).to.equal('display')
 
-      expect(users[1].username).to.equal('user_1')
-      expect(users[1].email).to.equal('user_1@example.com')
-      expect(users[1].nsfwPolicy).to.equal('display')
+      expect(data[1].username).to.equal('user_1')
+      expect(data[1].email).to.equal('user_1@example.com')
+      expect(data[1].nsfwPolicy).to.equal('display')
     })
 
     it('Should search user by username', async function () {
-      const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'oot')
-      const users = res.body.data as User[]
-
-      expect(res.body.total).to.equal(1)
-      expect(users.length).to.equal(1)
-
-      expect(users[0].username).to.equal('root')
+      const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', search: 'oot' })
+      expect(total).to.equal(1)
+      expect(data.length).to.equal(1)
+      expect(data[0].username).to.equal('root')
     })
 
     it('Should search user by email', async function () {
       {
-        const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'r_1@exam')
-        const users = res.body.data as User[]
-
-        expect(res.body.total).to.equal(1)
-        expect(users.length).to.equal(1)
-
-        expect(users[0].username).to.equal('user_1')
-        expect(users[0].email).to.equal('user_1@example.com')
+        const { total, data } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', search: 'r_1@exam' })
+        expect(total).to.equal(1)
+        expect(data.length).to.equal(1)
+        expect(data[0].username).to.equal('user_1')
+        expect(data[0].email).to.equal('user_1@example.com')
       }
 
       {
-        const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'example')
-        const users = res.body.data as User[]
-
-        expect(res.body.total).to.equal(2)
-        expect(users.length).to.equal(2)
-
-        expect(users[0].username).to.equal('root')
-        expect(users[1].username).to.equal('user_1')
+        const { total, data } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', search: 'example' })
+        expect(total).to.equal(2)
+        expect(data.length).to.equal(2)
+        expect(data[0].username).to.equal('root')
+        expect(data[1].username).to.equal('user_1')
       }
     })
   })
 
   describe('Update my account', function () {
+
     it('Should update my password', async function () {
-      await updateMyUser({
-        url: server.url,
-        accessToken: accessTokenUser,
+      await server.users.updateMe({
+        token: userToken,
         currentPassword: 'super password',
         password: 'new password'
       })
       user.password = 'new password'
 
-      await userLogin(server, user, HttpStatusCode.OK_200)
+      await server.login.login({ user })
     })
 
     it('Should be able to change the NSFW display attribute', async function () {
-      await updateMyUser({
-        url: server.url,
-        accessToken: accessTokenUser,
+      await server.users.updateMe({
+        token: userToken,
         nsfwPolicy: 'do_not_list'
       })
 
-      const res = await getMyUserInformation(server.url, accessTokenUser)
-      const user = res.body
-
+      const user = await server.users.getMyInfo({ token: userToken })
       expect(user.username).to.equal('user_1')
       expect(user.email).to.equal('user_1@example.com')
       expect(user.nsfwPolicy).to.equal('do_not_list')
@@ -606,42 +515,33 @@ describe('Test users', function () {
     })
 
     it('Should be able to change the autoPlayVideo attribute', async function () {
-      await updateMyUser({
-        url: server.url,
-        accessToken: accessTokenUser,
+      await server.users.updateMe({
+        token: userToken,
         autoPlayVideo: false
       })
 
-      const res = await getMyUserInformation(server.url, accessTokenUser)
-      const user = res.body
-
+      const user = await server.users.getMyInfo({ token: userToken })
       expect(user.autoPlayVideo).to.be.false
     })
 
     it('Should be able to change the autoPlayNextVideo attribute', async function () {
-      await updateMyUser({
-        url: server.url,
-        accessToken: accessTokenUser,
+      await server.users.updateMe({
+        token: userToken,
         autoPlayNextVideo: true
       })
 
-      const res = await getMyUserInformation(server.url, accessTokenUser)
-      const user = res.body
-
+      const user = await server.users.getMyInfo({ token: userToken })
       expect(user.autoPlayNextVideo).to.be.true
     })
 
     it('Should be able to change the email attribute', async function () {
-      await updateMyUser({
-        url: server.url,
-        accessToken: accessTokenUser,
+      await server.users.updateMe({
+        token: userToken,
         currentPassword: 'new password',
         email: 'updated@example.com'
       })
 
-      const res = await getMyUserInformation(server.url, accessTokenUser)
-      const user = res.body
-
+      const user = await server.users.getMyInfo({ token: userToken })
       expect(user.username).to.equal('user_1')
       expect(user.email).to.equal('updated@example.com')
       expect(user.nsfwPolicy).to.equal('do_not_list')
@@ -654,15 +554,9 @@ describe('Test users', function () {
     it('Should be able to update my avatar with a gif', async function () {
       const fixture = 'avatar.gif'
 
-      await updateMyAvatar({
-        url: server.url,
-        accessToken: accessTokenUser,
-        fixture
-      })
-
-      const res = await getMyUserInformation(server.url, accessTokenUser)
-      const user = res.body
+      await server.users.updateMyAvatar({ token: userToken, fixture })
 
+      const user = await server.users.getMyInfo({ token: userToken })
       await testImage(server.url, 'avatar-resized', user.account.avatar.path, '.gif')
     })
 
@@ -670,29 +564,17 @@ describe('Test users', function () {
       for (const extension of [ '.png', '.gif' ]) {
         const fixture = 'avatar' + extension
 
-        await updateMyAvatar({
-          url: server.url,
-          accessToken: accessTokenUser,
-          fixture
-        })
-
-        const res = await getMyUserInformation(server.url, accessTokenUser)
-        const user = res.body
+        await server.users.updateMyAvatar({ token: userToken, fixture })
 
+        const user = await server.users.getMyInfo({ token: userToken })
         await testImage(server.url, 'avatar-resized', user.account.avatar.path, extension)
       }
     })
 
     it('Should be able to update my display name', async function () {
-      await updateMyUser({
-        url: server.url,
-        accessToken: accessTokenUser,
-        displayName: 'new display name'
-      })
-
-      const res = await getMyUserInformation(server.url, accessTokenUser)
-      const user = res.body
+      await server.users.updateMe({ token: userToken, displayName: 'new display name' })
 
+      const user = await server.users.getMyInfo({ token: userToken })
       expect(user.username).to.equal('user_1')
       expect(user.email).to.equal('updated@example.com')
       expect(user.nsfwPolicy).to.equal('do_not_list')
@@ -703,15 +585,9 @@ describe('Test users', function () {
     })
 
     it('Should be able to update my description', async function () {
-      await updateMyUser({
-        url: server.url,
-        accessToken: accessTokenUser,
-        description: 'my super description updated'
-      })
-
-      const res = await getMyUserInformation(server.url, accessTokenUser)
-      const user: User = res.body
+      await server.users.updateMe({ token: userToken, description: 'my super description updated' })
 
+      const user = await server.users.getMyInfo({ token: userToken })
       expect(user.username).to.equal('user_1')
       expect(user.email).to.equal('updated@example.com')
       expect(user.nsfwPolicy).to.equal('do_not_list')
@@ -725,30 +601,21 @@ describe('Test users', function () {
 
     it('Should be able to update my theme', async function () {
       for (const theme of [ 'background-red', 'default', 'instance-default' ]) {
-        await updateMyUser({
-          url: server.url,
-          accessToken: accessTokenUser,
-          theme
-        })
+        await server.users.updateMe({ token: userToken, theme })
 
-        const res = await getMyUserInformation(server.url, accessTokenUser)
-        const body: User = res.body
-
-        expect(body.theme).to.equal(theme)
+        const user = await server.users.getMyInfo({ token: userToken })
+        expect(user.theme).to.equal(theme)
       }
     })
 
     it('Should be able to update my modal preferences', async function () {
-      await updateMyUser({
-        url: server.url,
-        accessToken: accessTokenUser,
+      await server.users.updateMe({
+        token: userToken,
         noInstanceConfigWarningModal: true,
         noWelcomeModal: true
       })
 
-      const res = await getMyUserInformation(server.url, accessTokenUser)
-      const user: User = res.body
-
+      const user = await server.users.getMyInfo({ token: userToken })
       expect(user.noWelcomeModal).to.be.true
       expect(user.noInstanceConfigWarningModal).to.be.true
     })
@@ -756,10 +623,9 @@ describe('Test users', function () {
 
   describe('Updating another user', function () {
     it('Should be able to update another user', async function () {
-      await updateUser({
-        url: server.url,
+      await server.users.update({
         userId,
-        accessToken,
+        token,
         email: 'updated2@example.com',
         emailVerified: true,
         videoQuota: 42,
@@ -768,8 +634,7 @@ describe('Test users', function () {
         pluginAuth: 'toto'
       })
 
-      const res = await getUserInformation(server.url, accessToken, userId)
-      const user = res.body as User
+      const user = await server.users.get({ token, userId })
 
       expect(user.username).to.equal('user_1')
       expect(user.email).to.equal('updated2@example.com')
@@ -783,57 +648,50 @@ describe('Test users', function () {
     })
 
     it('Should reset the auth plugin', async function () {
-      await updateUser({ url: server.url, userId, accessToken, pluginAuth: null })
+      await server.users.update({ userId, token, pluginAuth: null })
 
-      const res = await getUserInformation(server.url, accessToken, userId)
-      const user = res.body as User
+      const user = await server.users.get({ token, userId })
       expect(user.pluginAuth).to.be.null
     })
 
     it('Should have removed the user token', async function () {
-      await getMyUserVideoQuotaUsed(server.url, accessTokenUser, HttpStatusCode.UNAUTHORIZED_401)
+      await server.users.getMyQuotaUsed({ token: userToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
 
-      accessTokenUser = await userLogin(server, user)
+      userToken = await server.login.getAccessToken(user)
     })
 
     it('Should be able to update another user password', async function () {
-      await updateUser({
-        url: server.url,
-        userId,
-        accessToken,
-        password: 'password updated'
-      })
+      await server.users.update({ userId, token, password: 'password updated' })
 
-      await getMyUserVideoQuotaUsed(server.url, accessTokenUser, HttpStatusCode.UNAUTHORIZED_401)
+      await server.users.getMyQuotaUsed({ token: userToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
 
-      await userLogin(server, user, HttpStatusCode.BAD_REQUEST_400)
+      await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
 
       user.password = 'password updated'
-      accessTokenUser = await userLogin(server, user)
+      userToken = await server.login.getAccessToken(user)
     })
   })
 
   describe('Video blacklists', function () {
     it('Should be able to list video blacklist by a moderator', async function () {
-      await getBlacklistedVideosList({ url: server.url, token: accessTokenUser })
+      await server.blacklist.list({ token: userToken })
     })
   })
 
   describe('Remove a user', function () {
     it('Should be able to remove this user', async function () {
-      await removeUser(server.url, userId, accessToken)
+      await server.users.remove({ userId, token })
     })
 
     it('Should not be able to login with this user', async function () {
-      await userLogin(server, user, HttpStatusCode.BAD_REQUEST_400)
+      await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should not have videos of this user', async function () {
-      const res = await getVideosList(server.url)
-
-      expect(res.body.total).to.equal(1)
+      const { data, total } = await server.videos.list()
+      expect(total).to.equal(1)
 
-      const video = res.body.data[0]
+      const video = data[0]
       expect(video.account.name).to.equal('root')
     })
   })
@@ -845,7 +703,7 @@ describe('Test users', function () {
       const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' }
       const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' }
 
-      await registerUserWithChannel({ url: server.url, user, channel })
+      await server.users.register({ ...user, channel })
     })
 
     it('Should be able to login with this registered user', async function () {
@@ -854,40 +712,36 @@ describe('Test users', function () {
         password: 'my super password'
       }
 
-      user15AccessToken = await userLogin(server, user15)
+      user15AccessToken = await server.login.getAccessToken(user15)
     })
 
     it('Should have the correct display name', async function () {
-      const res = await getMyUserInformation(server.url, user15AccessToken)
-      const user: User = res.body
-
+      const user = await server.users.getMyInfo({ token: user15AccessToken })
       expect(user.account.displayName).to.equal('super user 15')
     })
 
     it('Should have the correct video quota', async function () {
-      const res = await getMyUserInformation(server.url, user15AccessToken)
-      const user = res.body
-
+      const user = await server.users.getMyInfo({ token: user15AccessToken })
       expect(user.videoQuota).to.equal(5 * 1024 * 1024)
     })
 
     it('Should have created the channel', async function () {
-      const res = await getVideoChannel(server.url, 'my_user_15_channel')
+      const { displayName } = await server.channels.get({ channelName: 'my_user_15_channel' })
 
-      expect(res.body.displayName).to.equal('my channel rocks')
+      expect(displayName).to.equal('my channel rocks')
     })
 
     it('Should remove me', async function () {
       {
-        const res = await getUsersList(server.url, server.accessToken)
-        expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined
+        const { data } = await server.users.list()
+        expect(data.find(u => u.username === 'user_15')).to.not.be.undefined
       }
 
-      await deleteMe(server.url, user15AccessToken)
+      await server.users.deleteMe({ token: user15AccessToken })
 
       {
-        const res = await getUsersList(server.url, server.accessToken)
-        expect(res.body.data.find(u => u.username === 'user_15')).to.be.undefined
+        const { data } = await server.users.list()
+        expect(data.find(u => u.username === 'user_15')).to.be.undefined
       }
     })
   })
@@ -901,49 +755,40 @@ describe('Test users', function () {
     }
 
     it('Should block a user', async function () {
-      const resUser = await createUser({
-        url: server.url,
-        accessToken: server.accessToken,
-        username: user16.username,
-        password: user16.password
-      })
-      user16Id = resUser.body.user.id
+      const user = await server.users.create({ ...user16 })
+      user16Id = user.id
 
-      user16AccessToken = await userLogin(server, user16)
+      user16AccessToken = await server.login.getAccessToken(user16)
 
-      await getMyUserInformation(server.url, user16AccessToken, HttpStatusCode.OK_200)
-      await blockUser(server.url, user16Id, server.accessToken)
+      await server.users.getMyInfo({ token: user16AccessToken, expectedStatus: HttpStatusCode.OK_200 })
+      await server.users.banUser({ userId: user16Id })
 
-      await getMyUserInformation(server.url, user16AccessToken, HttpStatusCode.UNAUTHORIZED_401)
-      await userLogin(server, user16, HttpStatusCode.BAD_REQUEST_400)
+      await server.users.getMyInfo({ token: user16AccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      await server.login.login({ user: user16, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should search user by banned status', async function () {
       {
-        const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', undefined, true)
-        const users = res.body.data as User[]
+        const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', blocked: true })
+        expect(total).to.equal(1)
+        expect(data.length).to.equal(1)
 
-        expect(res.body.total).to.equal(1)
-        expect(users.length).to.equal(1)
-
-        expect(users[0].username).to.equal(user16.username)
+        expect(data[0].username).to.equal(user16.username)
       }
 
       {
-        const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', undefined, false)
-        const users = res.body.data as User[]
-
-        expect(res.body.total).to.equal(1)
-        expect(users.length).to.equal(1)
+        const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', blocked: false })
+        expect(total).to.equal(1)
+        expect(data.length).to.equal(1)
 
-        expect(users[0].username).to.not.equal(user16.username)
+        expect(data[0].username).to.not.equal(user16.username)
       }
     })
 
     it('Should unblock a user', async function () {
-      await unblockUser(server.url, user16Id, server.accessToken)
-      user16AccessToken = await userLogin(server, user16)
-      await getMyUserInformation(server.url, user16AccessToken, HttpStatusCode.OK_200)
+      await server.users.unbanUser({ userId: user16Id })
+      user16AccessToken = await server.login.getAccessToken(user16)
+      await server.users.getMyInfo({ token: user16AccessToken, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -956,19 +801,12 @@ describe('Test users', function () {
         username: 'user_17',
         password: 'my super password'
       }
-      const resUser = await createUser({
-        url: server.url,
-        accessToken: server.accessToken,
-        username: user17.username,
-        password: user17.password
-      })
+      const created = await server.users.create({ ...user17 })
 
-      user17Id = resUser.body.user.id
-      user17AccessToken = await userLogin(server, user17)
-
-      const res = await getUserInformation(server.url, server.accessToken, user17Id, true)
-      const user: User = res.body
+      user17Id = created.id
+      user17AccessToken = await server.login.getAccessToken(user17)
 
+      const user = await server.users.get({ userId: user17Id, withStats: true })
       expect(user.videosCount).to.equal(0)
       expect(user.videoCommentsCount).to.equal(0)
       expect(user.abusesCount).to.equal(0)
@@ -977,54 +815,43 @@ describe('Test users', function () {
     })
 
     it('Should report correct videos count', async function () {
-      const videoAttributes = {
-        name: 'video to test user stats'
-      }
-      await uploadVideo(server.url, user17AccessToken, videoAttributes)
-      const res1 = await getVideosList(server.url)
-      videoId = res1.body.data.find(video => video.name === videoAttributes.name).id
+      const attributes = { name: 'video to test user stats' }
+      await server.videos.upload({ token: user17AccessToken, attributes })
 
-      const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
-      const user: User = res2.body
+      const { data } = await server.videos.list()
+      videoId = data.find(video => video.name === attributes.name).id
 
+      const user = await server.users.get({ userId: user17Id, withStats: true })
       expect(user.videosCount).to.equal(1)
     })
 
     it('Should report correct video comments for user', async function () {
       const text = 'super comment'
-      await addVideoCommentThread(server.url, user17AccessToken, videoId, text)
-
-      const res = await getUserInformation(server.url, server.accessToken, user17Id, true)
-      const user: User = res.body
+      await server.comments.createThread({ token: user17AccessToken, videoId, text })
 
+      const user = await server.users.get({ userId: user17Id, withStats: true })
       expect(user.videoCommentsCount).to.equal(1)
     })
 
     it('Should report correct abuses counts', async function () {
       const reason = 'my super bad reason'
-      await reportAbuse({ url: server.url, token: user17AccessToken, videoId, reason })
-
-      const res1 = await getAdminAbusesList({ url: server.url, token: server.accessToken })
-      const abuseId = res1.body.data[0].id
+      await server.abuses.report({ token: user17AccessToken, videoId, reason })
 
-      const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
-      const user2: User = res2.body
+      const body1 = await server.abuses.getAdminList()
+      const abuseId = body1.data[0].id
 
+      const user2 = await server.users.get({ userId: user17Id, withStats: true })
       expect(user2.abusesCount).to.equal(1) // number of incriminations
       expect(user2.abusesCreatedCount).to.equal(1) // number of reports created
 
-      const body: AbuseUpdate = { state: AbuseState.ACCEPTED }
-      await updateAbuse(server.url, server.accessToken, abuseId, body)
-
-      const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
-      const user3: User = res3.body
+      await server.abuses.update({ abuseId, body: { state: AbuseState.ACCEPTED } })
 
+      const user3 = await server.users.get({ userId: user17Id, withStats: true })
       expect(user3.abusesAcceptedCount).to.equal(1) // number of reports created accepted
     })
   })
 
   after(async function () {
-    await closeAllSequelize([ server ])
     await cleanupTests([ server ])
   })
 })
index 7ddbd5cd9c701736e04e6ed1c976711e30cbe73f..7fac6e7389e7e9ef20297f12c33f0b1fd2443eb7 100644 (file)
@@ -2,26 +2,16 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { join } from 'path'
 import { getAudioStream, getVideoStreamSize } from '@server/helpers/ffprobe-utils'
-import {
-  buildServerDirectory,
-  cleanupTests,
-  doubleFollow,
-  flushAndRunMultipleServers,
-  getVideo,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo,
-  waitJobs
-} from '../../../../shared/extra-utils'
-import { VideoDetails } from '../../../../shared/models/videos'
+import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test audio only video transcoding', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let videoUUID: string
+  let webtorrentAudioFileUrl: string
+  let fragmentedAudioFileUrl: string
 
   before(async function () {
     this.timeout(120000)
@@ -47,7 +37,7 @@ describe('Test audio only video transcoding', function () {
         }
       }
     }
-    servers = await flushAndRunMultipleServers(2, configOverride)
+    servers = await createMultipleServers(2, configOverride)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -59,15 +49,13 @@ describe('Test audio only video transcoding', function () {
   it('Should upload a video and transcode it', async function () {
     this.timeout(120000)
 
-    const resUpload = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'audio only' })
-    videoUUID = resUpload.body.video.uuid
+    const { uuid } = await servers[0].videos.upload({ attributes: { name: 'audio only' } })
+    videoUUID = uuid
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideo(server.url, videoUUID)
-      const video: VideoDetails = res.body
-
+      const video = await server.videos.get({ id: videoUUID })
       expect(video.streamingPlaylists).to.have.lengthOf(1)
 
       for (const files of [ video.files, video.streamingPlaylists[0].files ]) {
@@ -76,13 +64,18 @@ describe('Test audio only video transcoding', function () {
         expect(files[1].resolution.id).to.equal(240)
         expect(files[2].resolution.id).to.equal(0)
       }
+
+      if (server.serverNumber === 1) {
+        webtorrentAudioFileUrl = video.files[2].fileUrl
+        fragmentedAudioFileUrl = video.streamingPlaylists[0].files[2].fileUrl
+      }
     }
   })
 
   it('0p transcoded video should not have video', async function () {
     const paths = [
-      buildServerDirectory(servers[0], join('videos', videoUUID + '-0.mp4')),
-      buildServerDirectory(servers[0], join('streaming-playlists', 'hls', videoUUID, videoUUID + '-0-fragmented.mp4'))
+      servers[0].servers.buildWebTorrentFilePath(webtorrentAudioFileUrl),
+      servers[0].servers.buildFragmentedFilePath(videoUUID, fragmentedAudioFileUrl)
     ]
 
     for (const path of paths) {
index a8c8a889b11e8774f3be5322018fafe69c6ec557..f9220e4b34a780e2470ed09904fc20414075eda7 100644 (file)
@@ -3,49 +3,29 @@
 import 'mocha'
 import * as chai from 'chai'
 import * as request from 'supertest'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
-  addVideoChannel,
   buildAbsoluteFixturePath,
   checkTmpIsEmpty,
   checkVideoFilesWereRemoved,
   cleanupTests,
   completeVideoCheck,
-  createUser,
+  createMultipleServers,
   dateIsValid,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getLocalVideos,
-  getVideo,
-  getVideoChannelsList,
-  getVideosList,
-  rateVideo,
-  removeVideo,
-  ServerInfo,
+  PeerTubeServer,
+  saveVideoInServers,
   setAccessTokensToServers,
   testImage,
-  updateVideo,
-  uploadVideo,
-  userLogin,
-  viewVideo,
   wait,
+  waitJobs,
   webtorrentAdd
-} from '../../../../shared/extra-utils'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import {
-  addVideoCommentReply,
-  addVideoCommentThread,
-  deleteVideoComment,
-  findCommentId,
-  getVideoCommentThreads,
-  getVideoThreadComments
-} from '../../../../shared/extra-utils/videos/video-comments'
-import { VideoComment, VideoCommentThreadTree, VideoPrivacy } from '../../../../shared/models/videos'
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoCommentThreadTree, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test multiple servers', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   const toRemove = []
   let videoUUID = ''
   let videoChannelId: number
@@ -53,7 +33,7 @@ describe('Test multiple servers', function () {
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await createMultipleServers(3)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -64,9 +44,9 @@ describe('Test multiple servers', function () {
         displayName: 'my channel',
         description: 'super channel'
       }
-      await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel)
-      const channelRes = await getVideoChannelsList(servers[0].url, 0, 1)
-      videoChannelId = channelRes.body.data[0].id
+      await servers[0].channels.create({ attributes: videoChannel })
+      const { data } = await servers[0].channels.list({ start: 0, count: 1 })
+      videoChannelId = data[0].id
     }
 
     // Server 1 and server 2 follow each other
@@ -79,10 +59,9 @@ describe('Test multiple servers', function () {
 
   it('Should not have videos for all servers', async function () {
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-      const videos = res.body.data
-      expect(videos).to.be.an('array')
-      expect(videos.length).to.equal(0)
+      const { data } = await server.videos.list()
+      expect(data).to.be.an('array')
+      expect(data.length).to.equal(0)
     }
   })
 
@@ -90,7 +69,7 @@ describe('Test multiple servers', function () {
     it('Should upload the video on server 1 and propagate on each server', async function () {
       this.timeout(25000)
 
-      const videoAttributes = {
+      const attributes = {
         name: 'my super name for server 1',
         category: 5,
         licence: 4,
@@ -103,7 +82,7 @@ describe('Test multiple servers', function () {
         channelId: videoChannelId,
         fixture: 'video_short1.webm'
       }
-      await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
+      await servers[0].videos.upload({ attributes })
 
       await waitJobs(servers)
 
@@ -146,14 +125,13 @@ describe('Test multiple servers', function () {
           ]
         }
 
-        const res = await getVideosList(server.url)
-        const videos = res.body.data
-        expect(videos).to.be.an('array')
-        expect(videos.length).to.equal(1)
-        const video = videos[0]
+        const { data } = await server.videos.list()
+        expect(data).to.be.an('array')
+        expect(data.length).to.equal(1)
+        const video = data[0]
 
-        await completeVideoCheck(server.url, video, checkAttributes)
-        publishedAt = video.publishedAt
+        await completeVideoCheck(server, video, checkAttributes)
+        publishedAt = video.publishedAt as string
       }
     })
 
@@ -164,10 +142,10 @@ describe('Test multiple servers', function () {
         username: 'user1',
         password: 'super_password'
       }
-      await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
-      const userAccessToken = await userLogin(servers[1], user)
+      await servers[1].users.create({ username: user.username, password: user.password })
+      const userAccessToken = await servers[1].login.getAccessToken(user)
 
-      const videoAttributes = {
+      const attributes = {
         name: 'my super name for server 2',
         category: 4,
         licence: 3,
@@ -180,7 +158,7 @@ describe('Test multiple servers', function () {
         thumbnailfile: 'thumbnail.jpg',
         previewfile: 'preview.jpg'
       }
-      await uploadVideo(servers[1].url, userAccessToken, videoAttributes, HttpStatusCode.OK_200, 'resumable')
+      await servers[1].videos.upload({ token: userAccessToken, attributes, mode: 'resumable' })
 
       // Transcoding
       await waitJobs(servers)
@@ -235,65 +213,67 @@ describe('Test multiple servers', function () {
           previewfile: 'preview'
         }
 
-        const res = await getVideosList(server.url)
-        const videos = res.body.data
-        expect(videos).to.be.an('array')
-        expect(videos.length).to.equal(2)
-        const video = videos[1]
+        const { data } = await server.videos.list()
+        expect(data).to.be.an('array')
+        expect(data.length).to.equal(2)
+        const video = data[1]
 
-        await completeVideoCheck(server.url, video, checkAttributes)
+        await completeVideoCheck(server, video, checkAttributes)
       }
     })
 
     it('Should upload two videos on server 3 and propagate on each server', async function () {
       this.timeout(45000)
 
-      const videoAttributes1 = {
-        name: 'my super name for server 3',
-        category: 6,
-        licence: 5,
-        language: 'de',
-        nsfw: true,
-        description: 'my super description for server 3',
-        support: 'my super support text for server 3',
-        tags: [ 'tag1p3' ],
-        fixture: 'video_short3.webm'
+      {
+        const attributes = {
+          name: 'my super name for server 3',
+          category: 6,
+          licence: 5,
+          language: 'de',
+          nsfw: true,
+          description: 'my super description for server 3',
+          support: 'my super support text for server 3',
+          tags: [ 'tag1p3' ],
+          fixture: 'video_short3.webm'
+        }
+        await servers[2].videos.upload({ attributes })
       }
-      await uploadVideo(servers[2].url, servers[2].accessToken, videoAttributes1)
-
-      const videoAttributes2 = {
-        name: 'my super name for server 3-2',
-        category: 7,
-        licence: 6,
-        language: 'ko',
-        nsfw: false,
-        description: 'my super description for server 3-2',
-        support: 'my super support text for server 3-2',
-        tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
-        fixture: 'video_short.webm'
+
+      {
+        const attributes = {
+          name: 'my super name for server 3-2',
+          category: 7,
+          licence: 6,
+          language: 'ko',
+          nsfw: false,
+          description: 'my super description for server 3-2',
+          support: 'my super support text for server 3-2',
+          tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
+          fixture: 'video_short.webm'
+        }
+        await servers[2].videos.upload({ attributes })
       }
-      await uploadVideo(servers[2].url, servers[2].accessToken, videoAttributes2)
 
       await waitJobs(servers)
 
       // All servers should have this video
       for (const server of servers) {
         const isLocal = server.url === 'http://localhost:' + servers[2].port
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        const videos = res.body.data
-        expect(videos).to.be.an('array')
-        expect(videos.length).to.equal(4)
+        expect(data).to.be.an('array')
+        expect(data.length).to.equal(4)
 
         // We not sure about the order of the two last uploads
         let video1 = null
         let video2 = null
-        if (videos[2].name === 'my super name for server 3') {
-          video1 = videos[2]
-          video2 = videos[3]
+        if (data[2].name === 'my super name for server 3') {
+          video1 = data[2]
+          video2 = data[3]
         } else {
-          video1 = videos[3]
-          video2 = videos[2]
+          video1 = data[3]
+          video2 = data[2]
         }
 
         const checkAttributesVideo1 = {
@@ -328,7 +308,7 @@ describe('Test multiple servers', function () {
             }
           ]
         }
-        await completeVideoCheck(server.url, video1, checkAttributesVideo1)
+        await completeVideoCheck(server, video1, checkAttributesVideo1)
 
         const checkAttributesVideo2 = {
           name: 'my super name for server 3-2',
@@ -362,38 +342,38 @@ describe('Test multiple servers', function () {
             }
           ]
         }
-        await completeVideoCheck(server.url, video2, checkAttributesVideo2)
+        await completeVideoCheck(server, video2, checkAttributesVideo2)
       }
     })
   })
 
   describe('It should list local videos', function () {
     it('Should list only local videos on server 1', async function () {
-      const { body } = await getLocalVideos(servers[0].url)
+      const { data, total } = await servers[0].videos.list({ filter: 'local' })
 
-      expect(body.total).to.equal(1)
-      expect(body.data).to.be.an('array')
-      expect(body.data.length).to.equal(1)
-      expect(body.data[0].name).to.equal('my super name for server 1')
+      expect(total).to.equal(1)
+      expect(data).to.be.an('array')
+      expect(data.length).to.equal(1)
+      expect(data[0].name).to.equal('my super name for server 1')
     })
 
     it('Should list only local videos on server 2', async function () {
-      const { body } = await getLocalVideos(servers[1].url)
+      const { data, total } = await servers[1].videos.list({ filter: 'local' })
 
-      expect(body.total).to.equal(1)
-      expect(body.data).to.be.an('array')
-      expect(body.data.length).to.equal(1)
-      expect(body.data[0].name).to.equal('my super name for server 2')
+      expect(total).to.equal(1)
+      expect(data).to.be.an('array')
+      expect(data.length).to.equal(1)
+      expect(data[0].name).to.equal('my super name for server 2')
     })
 
     it('Should list only local videos on server 3', async function () {
-      const { body } = await getLocalVideos(servers[2].url)
+      const { data, total } = await servers[2].videos.list({ filter: 'local' })
 
-      expect(body.total).to.equal(2)
-      expect(body.data).to.be.an('array')
-      expect(body.data.length).to.equal(2)
-      expect(body.data[0].name).to.equal('my super name for server 3')
-      expect(body.data[1].name).to.equal('my super name for server 3-2')
+      expect(total).to.equal(2)
+      expect(data).to.be.an('array')
+      expect(data.length).to.equal(2)
+      expect(data[0].name).to.equal('my super name for server 3')
+      expect(data[1].name).to.equal('my super name for server 3-2')
     })
   })
 
@@ -401,15 +381,13 @@ describe('Test multiple servers', function () {
     it('Should add the file 1 by asking server 3', async function () {
       this.timeout(10000)
 
-      const res = await getVideosList(servers[2].url)
-
-      const video = res.body.data[0]
-      toRemove.push(res.body.data[2])
-      toRemove.push(res.body.data[3])
+      const { data } = await servers[2].videos.list()
 
-      const res2 = await getVideo(servers[2].url, video.id)
-      const videoDetails = res2.body
+      const video = data[0]
+      toRemove.push(data[2])
+      toRemove.push(data[3])
 
+      const videoDetails = await servers[2].videos.get({ id: video.id })
       const torrent = await webtorrentAdd(videoDetails.files[0].magnetUri, true)
       expect(torrent.files).to.be.an('array')
       expect(torrent.files.length).to.equal(1)
@@ -419,11 +397,10 @@ describe('Test multiple servers', function () {
     it('Should add the file 2 by asking server 1', async function () {
       this.timeout(10000)
 
-      const res = await getVideosList(servers[0].url)
+      const { data } = await servers[0].videos.list()
 
-      const video = res.body.data[1]
-      const res2 = await getVideo(servers[0].url, video.id)
-      const videoDetails = res2.body
+      const video = data[1]
+      const videoDetails = await servers[0].videos.get({ id: video.id })
 
       const torrent = await webtorrentAdd(videoDetails.files[0].magnetUri, true)
       expect(torrent.files).to.be.an('array')
@@ -434,11 +411,10 @@ describe('Test multiple servers', function () {
     it('Should add the file 3 by asking server 2', async function () {
       this.timeout(10000)
 
-      const res = await getVideosList(servers[1].url)
+      const { data } = await servers[1].videos.list()
 
-      const video = res.body.data[2]
-      const res2 = await getVideo(servers[1].url, video.id)
-      const videoDetails = res2.body
+      const video = data[2]
+      const videoDetails = await servers[1].videos.get({ id: video.id })
 
       const torrent = await webtorrentAdd(videoDetails.files[0].magnetUri, true)
       expect(torrent.files).to.be.an('array')
@@ -449,11 +425,10 @@ describe('Test multiple servers', function () {
     it('Should add the file 3-2 by asking server 1', async function () {
       this.timeout(10000)
 
-      const res = await getVideosList(servers[0].url)
+      const { data } = await servers[0].videos.list()
 
-      const video = res.body.data[3]
-      const res2 = await getVideo(servers[0].url, video.id)
-      const videoDetails = res2.body
+      const video = data[3]
+      const videoDetails = await servers[0].videos.get({ id: video.id })
 
       const torrent = await webtorrentAdd(videoDetails.files[0].magnetUri)
       expect(torrent.files).to.be.an('array')
@@ -464,11 +439,10 @@ describe('Test multiple servers', function () {
     it('Should add the file 2 in 360p by asking server 1', async function () {
       this.timeout(10000)
 
-      const res = await getVideosList(servers[0].url)
+      const { data } = await servers[0].videos.list()
 
-      const video = res.body.data.find(v => v.name === 'my super name for server 2')
-      const res2 = await getVideo(servers[0].url, video.id)
-      const videoDetails = res2.body
+      const video = data.find(v => v.name === 'my super name for server 2')
+      const videoDetails = await servers[0].videos.get({ id: video.id })
 
       const file = videoDetails.files.find(f => f.resolution.id === 360)
       expect(file).not.to.be.undefined
@@ -487,30 +461,36 @@ describe('Test multiple servers', function () {
     let remoteVideosServer3 = []
 
     before(async function () {
-      const res1 = await getVideosList(servers[0].url)
-      remoteVideosServer1 = res1.body.data.filter(video => video.isLocal === false).map(video => video.uuid)
+      {
+        const { data } = await servers[0].videos.list()
+        remoteVideosServer1 = data.filter(video => video.isLocal === false).map(video => video.uuid)
+      }
 
-      const res2 = await getVideosList(servers[1].url)
-      remoteVideosServer2 = res2.body.data.filter(video => video.isLocal === false).map(video => video.uuid)
+      {
+        const { data } = await servers[1].videos.list()
+        remoteVideosServer2 = data.filter(video => video.isLocal === false).map(video => video.uuid)
+      }
 
-      const res3 = await getVideosList(servers[2].url)
-      localVideosServer3 = res3.body.data.filter(video => video.isLocal === true).map(video => video.uuid)
-      remoteVideosServer3 = res3.body.data.filter(video => video.isLocal === false).map(video => video.uuid)
+      {
+        const { data } = await servers[2].videos.list()
+        localVideosServer3 = data.filter(video => video.isLocal === true).map(video => video.uuid)
+        remoteVideosServer3 = data.filter(video => video.isLocal === false).map(video => video.uuid)
+      }
     })
 
     it('Should view multiple videos on owned servers', async function () {
       this.timeout(30000)
 
-      await viewVideo(servers[2].url, localVideosServer3[0])
+      await servers[2].videos.view({ id: localVideosServer3[0] })
       await wait(1000)
 
-      await viewVideo(servers[2].url, localVideosServer3[0])
-      await viewVideo(servers[2].url, localVideosServer3[1])
+      await servers[2].videos.view({ id: localVideosServer3[0] })
+      await servers[2].videos.view({ id: localVideosServer3[1] })
 
       await wait(1000)
 
-      await viewVideo(servers[2].url, localVideosServer3[0])
-      await viewVideo(servers[2].url, localVideosServer3[0])
+      await servers[2].videos.view({ id: localVideosServer3[0] })
+      await servers[2].videos.view({ id: localVideosServer3[0] })
 
       await waitJobs(servers)
 
@@ -520,11 +500,10 @@ describe('Test multiple servers', function () {
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        const videos = res.body.data
-        const video0 = videos.find(v => v.uuid === localVideosServer3[0])
-        const video1 = videos.find(v => v.uuid === localVideosServer3[1])
+        const video0 = data.find(v => v.uuid === localVideosServer3[0])
+        const video1 = data.find(v => v.uuid === localVideosServer3[1])
 
         expect(video0.views).to.equal(3)
         expect(video1.views).to.equal(1)
@@ -535,16 +514,16 @@ describe('Test multiple servers', function () {
       this.timeout(45000)
 
       const tasks: Promise<any>[] = []
-      tasks.push(viewVideo(servers[0].url, remoteVideosServer1[0]))
-      tasks.push(viewVideo(servers[1].url, remoteVideosServer2[0]))
-      tasks.push(viewVideo(servers[1].url, remoteVideosServer2[0]))
-      tasks.push(viewVideo(servers[2].url, remoteVideosServer3[0]))
-      tasks.push(viewVideo(servers[2].url, remoteVideosServer3[1]))
-      tasks.push(viewVideo(servers[2].url, remoteVideosServer3[1]))
-      tasks.push(viewVideo(servers[2].url, remoteVideosServer3[1]))
-      tasks.push(viewVideo(servers[2].url, localVideosServer3[1]))
-      tasks.push(viewVideo(servers[2].url, localVideosServer3[1]))
-      tasks.push(viewVideo(servers[2].url, localVideosServer3[1]))
+      tasks.push(servers[0].videos.view({ id: remoteVideosServer1[0] }))
+      tasks.push(servers[1].videos.view({ id: remoteVideosServer2[0] }))
+      tasks.push(servers[1].videos.view({ id: remoteVideosServer2[0] }))
+      tasks.push(servers[2].videos.view({ id: remoteVideosServer3[0] }))
+      tasks.push(servers[2].videos.view({ id: remoteVideosServer3[1] }))
+      tasks.push(servers[2].videos.view({ id: remoteVideosServer3[1] }))
+      tasks.push(servers[2].videos.view({ id: remoteVideosServer3[1] }))
+      tasks.push(servers[2].videos.view({ id: localVideosServer3[1] }))
+      tasks.push(servers[2].videos.view({ id: localVideosServer3[1] }))
+      tasks.push(servers[2].videos.view({ id: localVideosServer3[1] }))
 
       await Promise.all(tasks)
 
@@ -558,18 +537,16 @@ describe('Test multiple servers', function () {
       let baseVideos = null
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-
-        const videos = res.body.data
+        const { data } = await server.videos.list()
 
         // Initialize base videos for future comparisons
         if (baseVideos === null) {
-          baseVideos = videos
+          baseVideos = data
           continue
         }
 
         for (const baseVideo of baseVideos) {
-          const sameVideo = videos.find(video => video.name === baseVideo.name)
+          const sameVideo = data.find(video => video.name === baseVideo.name)
           expect(baseVideo.views).to.equal(sameVideo.views)
         }
       }
@@ -578,35 +555,34 @@ describe('Test multiple servers', function () {
     it('Should like and dislikes videos on different services', async function () {
       this.timeout(50000)
 
-      await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'like')
+      await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'like' })
       await wait(500)
-      await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'dislike')
+      await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'dislike' })
       await wait(500)
-      await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'like')
-      await rateVideo(servers[2].url, servers[2].accessToken, localVideosServer3[1], 'like')
+      await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'like' })
+      await servers[2].videos.rate({ id: localVideosServer3[1], rating: 'like' })
       await wait(500)
-      await rateVideo(servers[2].url, servers[2].accessToken, localVideosServer3[1], 'dislike')
-      await rateVideo(servers[2].url, servers[2].accessToken, remoteVideosServer3[1], 'dislike')
+      await servers[2].videos.rate({ id: localVideosServer3[1], rating: 'dislike' })
+      await servers[2].videos.rate({ id: remoteVideosServer3[1], rating: 'dislike' })
       await wait(500)
-      await rateVideo(servers[2].url, servers[2].accessToken, remoteVideosServer3[0], 'like')
+      await servers[2].videos.rate({ id: remoteVideosServer3[0], rating: 'like' })
 
       await waitJobs(servers)
       await wait(5000)
+      await waitJobs(servers)
 
       let baseVideos = null
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-
-        const videos = res.body.data
+        const { data } = await server.videos.list()
 
         // Initialize base videos for future comparisons
         if (baseVideos === null) {
-          baseVideos = videos
+          baseVideos = data
           continue
         }
 
         for (const baseVideo of baseVideos) {
-          const sameVideo = videos.find(video => video.name === baseVideo.name)
+          const sameVideo = data.find(video => video.name === baseVideo.name)
           expect(baseVideo.likes).to.equal(sameVideo.likes)
           expect(baseVideo.dislikes).to.equal(sameVideo.dislikes)
         }
@@ -632,7 +608,7 @@ describe('Test multiple servers', function () {
         previewfile: 'preview.jpg'
       }
 
-      await updateVideo(servers[2].url, servers[2].accessToken, toRemove[0].id, attributes)
+      await servers[2].videos.update({ id: toRemove[0].id, attributes })
 
       await waitJobs(servers)
     })
@@ -641,10 +617,9 @@ describe('Test multiple servers', function () {
       this.timeout(10000)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        const videos = res.body.data
-        const videoUpdated = videos.find(video => video.name === 'my super video updated')
+        const videoUpdated = data.find(video => video.name === 'my super video updated')
         expect(!!videoUpdated).to.be.true
 
         const isLocal = server.url === 'http://localhost:' + servers[2].port
@@ -683,49 +658,46 @@ describe('Test multiple servers', function () {
           thumbnailfile: 'thumbnail',
           previewfile: 'preview'
         }
-        await completeVideoCheck(server.url, videoUpdated, checkAttributes)
+        await completeVideoCheck(server, videoUpdated, checkAttributes)
       }
     })
 
-    it('Should remove the videos 3 and 3-2 by asking server 3', async function () {
-      this.timeout(10000)
+    it('Should remove the videos 3 and 3-2 by asking server 3 and correctly delete files', async function () {
+      this.timeout(30000)
 
-      await removeVideo(servers[2].url, servers[2].accessToken, toRemove[0].id)
-      await removeVideo(servers[2].url, servers[2].accessToken, toRemove[1].id)
+      for (const id of [ toRemove[0].id, toRemove[1].id ]) {
+        await saveVideoInServers(servers, id)
 
-      await waitJobs(servers)
-    })
+        await servers[2].videos.remove({ id })
 
-    it('Should not have files of videos 3 and 3-2 on each server', async function () {
-      for (const server of servers) {
-        await checkVideoFilesWereRemoved(toRemove[0].uuid, server.internalServerNumber)
-        await checkVideoFilesWereRemoved(toRemove[1].uuid, server.internalServerNumber)
+        await waitJobs(servers)
+
+        for (const server of servers) {
+          await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
+        }
       }
     })
 
     it('Should have videos 1 and 3 on each server', async function () {
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-
-        const videos = res.body.data
-        expect(videos).to.be.an('array')
-        expect(videos.length).to.equal(2)
-        expect(videos[0].name).not.to.equal(videos[1].name)
-        expect(videos[0].name).not.to.equal(toRemove[0].name)
-        expect(videos[1].name).not.to.equal(toRemove[0].name)
-        expect(videos[0].name).not.to.equal(toRemove[1].name)
-        expect(videos[1].name).not.to.equal(toRemove[1].name)
-
-        videoUUID = videos.find(video => video.name === 'my super name for server 1').uuid
+        const { data } = await server.videos.list()
+
+        expect(data).to.be.an('array')
+        expect(data.length).to.equal(2)
+        expect(data[0].name).not.to.equal(data[1].name)
+        expect(data[0].name).not.to.equal(toRemove[0].name)
+        expect(data[1].name).not.to.equal(toRemove[0].name)
+        expect(data[0].name).not.to.equal(toRemove[1].name)
+        expect(data[1].name).not.to.equal(toRemove[1].name)
+
+        videoUUID = data.find(video => video.name === 'my super name for server 1').uuid
       }
     })
 
     it('Should get the same video by UUID on each server', async function () {
       let baseVideo = null
       for (const server of servers) {
-        const res = await getVideo(server.url, videoUUID)
-
-        const video = res.body
+        const video = await server.videos.get({ id: videoUUID })
 
         if (baseVideo === null) {
           baseVideo = video
@@ -748,8 +720,7 @@ describe('Test multiple servers', function () {
 
     it('Should get the preview from each server', async function () {
       for (const server of servers) {
-        const res = await getVideo(server.url, videoUUID)
-        const video = res.body
+        const video = await server.videos.get({ id: videoUUID })
 
         await testImage(server.url, 'video_short1-preview.webm', video.previewPath)
       }
@@ -764,36 +735,36 @@ describe('Test multiple servers', function () {
 
       {
         const text = 'my super first comment'
-        await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, text)
+        await servers[0].comments.createThread({ videoId: videoUUID, text })
       }
 
       {
         const text = 'my super second comment'
-        await addVideoCommentThread(servers[2].url, servers[2].accessToken, videoUUID, text)
+        await servers[2].comments.createThread({ videoId: videoUUID, text })
       }
 
       await waitJobs(servers)
 
       {
-        const threadId = await findCommentId(servers[1].url, videoUUID, 'my super first comment')
+        const threadId = await servers[1].comments.findCommentId({ videoId: videoUUID, text: 'my super first comment' })
 
         const text = 'my super answer to thread 1'
-        await addVideoCommentReply(servers[1].url, servers[1].accessToken, videoUUID, threadId, text)
+        await servers[1].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text })
       }
 
       await waitJobs(servers)
 
       {
-        const threadId = await findCommentId(servers[2].url, videoUUID, 'my super first comment')
+        const threadId = await servers[2].comments.findCommentId({ videoId: videoUUID, text: 'my super first comment' })
 
-        const res2 = await getVideoThreadComments(servers[2].url, videoUUID, threadId)
-        const childCommentId = res2.body.children[0].comment.id
+        const body = await servers[2].comments.getThread({ videoId: videoUUID, threadId })
+        const childCommentId = body.children[0].comment.id
 
         const text3 = 'my second answer to thread 1'
-        await addVideoCommentReply(servers[2].url, servers[2].accessToken, videoUUID, threadId, text3)
+        await servers[2].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text: text3 })
 
         const text2 = 'my super answer to answer of thread 1'
-        await addVideoCommentReply(servers[2].url, servers[2].accessToken, videoUUID, childCommentId, text2)
+        await servers[2].comments.addReply({ videoId: videoUUID, toCommentId: childCommentId, text: text2 })
       }
 
       await waitJobs(servers)
@@ -801,14 +772,14 @@ describe('Test multiple servers', function () {
 
     it('Should have these threads', async function () {
       for (const server of servers) {
-        const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
+        const body = await server.comments.listThreads({ videoId: videoUUID })
 
-        expect(res.body.total).to.equal(2)
-        expect(res.body.data).to.be.an('array')
-        expect(res.body.data).to.have.lengthOf(2)
+        expect(body.total).to.equal(2)
+        expect(body.data).to.be.an('array')
+        expect(body.data).to.have.lengthOf(2)
 
         {
-          const comment: VideoComment = res.body.data.find(c => c.text === 'my super first comment')
+          const comment = body.data.find(c => c.text === 'my super first comment')
           expect(comment).to.not.be.undefined
           expect(comment.inReplyToCommentId).to.be.null
           expect(comment.account.name).to.equal('root')
@@ -819,7 +790,7 @@ describe('Test multiple servers', function () {
         }
 
         {
-          const comment: VideoComment = res.body.data.find(c => c.text === 'my super second comment')
+          const comment = body.data.find(c => c.text === 'my super second comment')
           expect(comment).to.not.be.undefined
           expect(comment.inReplyToCommentId).to.be.null
           expect(comment.account.name).to.equal('root')
@@ -833,12 +804,11 @@ describe('Test multiple servers', function () {
 
     it('Should have these comments', async function () {
       for (const server of servers) {
-        const res1 = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
-        const threadId = res1.body.data.find(c => c.text === 'my super first comment').id
+        const body = await server.comments.listThreads({ videoId: videoUUID })
+        const threadId = body.data.find(c => c.text === 'my super first comment').id
 
-        const res2 = await getVideoThreadComments(server.url, videoUUID, threadId)
+        const tree = await server.comments.getThread({ videoId: videoUUID, threadId })
 
-        const tree: VideoCommentThreadTree = res2.body
         expect(tree.comment.text).equal('my super first comment')
         expect(tree.comment.account.name).equal('root')
         expect(tree.comment.account.host).equal('localhost:' + servers[0].port)
@@ -867,19 +837,17 @@ describe('Test multiple servers', function () {
     it('Should delete a reply', async function () {
       this.timeout(10000)
 
-      await deleteVideoComment(servers[2].url, servers[2].accessToken, videoUUID, childOfFirstChild.comment.id)
+      await servers[2].comments.delete({ videoId: videoUUID, commentId: childOfFirstChild.comment.id })
 
       await waitJobs(servers)
     })
 
     it('Should have this comment marked as deleted', async function () {
       for (const server of servers) {
-        const res1 = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
-        const threadId = res1.body.data.find(c => c.text === 'my super first comment').id
-
-        const res2 = await getVideoThreadComments(server.url, videoUUID, threadId)
+        const { data } = await server.comments.listThreads({ videoId: videoUUID })
+        const threadId = data.find(c => c.text === 'my super first comment').id
 
-        const tree: VideoCommentThreadTree = res2.body
+        const tree = await server.comments.getThread({ videoId: videoUUID, threadId })
         expect(tree.comment.text).equal('my super first comment')
 
         const firstChild = tree.children[0]
@@ -900,23 +868,23 @@ describe('Test multiple servers', function () {
     it('Should delete the thread comments', async function () {
       this.timeout(10000)
 
-      const res = await getVideoCommentThreads(servers[0].url, videoUUID, 0, 5)
-      const threadId = res.body.data.find(c => c.text === 'my super first comment').id
-      await deleteVideoComment(servers[0].url, servers[0].accessToken, videoUUID, threadId)
+      const { data } = await servers[0].comments.listThreads({ videoId: videoUUID })
+      const commentId = data.find(c => c.text === 'my super first comment').id
+      await servers[0].comments.delete({ videoId: videoUUID, commentId })
 
       await waitJobs(servers)
     })
 
     it('Should have the threads marked as deleted on other servers too', async function () {
       for (const server of servers) {
-        const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
+        const body = await server.comments.listThreads({ videoId: videoUUID })
 
-        expect(res.body.total).to.equal(2)
-        expect(res.body.data).to.be.an('array')
-        expect(res.body.data).to.have.lengthOf(2)
+        expect(body.total).to.equal(2)
+        expect(body.data).to.be.an('array')
+        expect(body.data).to.have.lengthOf(2)
 
         {
-          const comment: VideoComment = res.body.data[0]
+          const comment = body.data[0]
           expect(comment).to.not.be.undefined
           expect(comment.inReplyToCommentId).to.be.null
           expect(comment.account.name).to.equal('root')
@@ -927,7 +895,7 @@ describe('Test multiple servers', function () {
         }
 
         {
-          const deletedComment: VideoComment = res.body.data[1]
+          const deletedComment = body.data[1]
           expect(deletedComment).to.not.be.undefined
           expect(deletedComment.isDeleted).to.be.true
           expect(deletedComment.deletedAt).to.not.be.null
@@ -945,22 +913,22 @@ describe('Test multiple servers', function () {
     it('Should delete a remote thread by the origin server', async function () {
       this.timeout(5000)
 
-      const res = await getVideoCommentThreads(servers[0].url, videoUUID, 0, 5)
-      const threadId = res.body.data.find(c => c.text === 'my super second comment').id
-      await deleteVideoComment(servers[0].url, servers[0].accessToken, videoUUID, threadId)
+      const { data } = await servers[0].comments.listThreads({ videoId: videoUUID })
+      const commentId = data.find(c => c.text === 'my super second comment').id
+      await servers[0].comments.delete({ videoId: videoUUID, commentId })
 
       await waitJobs(servers)
     })
 
     it('Should have the threads marked as deleted on other servers too', async function () {
       for (const server of servers) {
-        const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
+        const body = await server.comments.listThreads({ videoId: videoUUID })
 
-        expect(res.body.total).to.equal(2)
-        expect(res.body.data).to.have.lengthOf(2)
+        expect(body.total).to.equal(2)
+        expect(body.data).to.have.lengthOf(2)
 
         {
-          const comment: VideoComment = res.body.data[0]
+          const comment = body.data[0]
           expect(comment.text).to.equal('')
           expect(comment.isDeleted).to.be.true
           expect(comment.createdAt).to.not.be.null
@@ -970,7 +938,7 @@ describe('Test multiple servers', function () {
         }
 
         {
-          const comment: VideoComment = res.body.data[1]
+          const comment = body.data[1]
           expect(comment.text).to.equal('')
           expect(comment.isDeleted).to.be.true
           expect(comment.createdAt).to.not.be.null
@@ -989,17 +957,17 @@ describe('Test multiple servers', function () {
         downloadEnabled: false
       }
 
-      await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, attributes)
+      await servers[0].videos.update({ id: videoUUID, attributes })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideo(server.url, videoUUID)
-        expect(res.body.commentsEnabled).to.be.false
-        expect(res.body.downloadEnabled).to.be.false
+        const video = await server.videos.get({ id: videoUUID })
+        expect(video.commentsEnabled).to.be.false
+        expect(video.downloadEnabled).to.be.false
 
         const text = 'my super forbidden comment'
-        await addVideoCommentThread(server.url, server.accessToken, videoUUID, text, HttpStatusCode.CONFLICT_409)
+        await server.comments.createThread({ videoId: videoUUID, text, expectedStatus: HttpStatusCode.CONFLICT_409 })
       }
     })
   })
@@ -1024,8 +992,8 @@ describe('Test multiple servers', function () {
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-        const video = res.body.data.find(v => v.name === 'minimum parameters')
+        const { data } = await server.videos.list()
+        const video = data.find(v => v.name === 'minimum parameters')
 
         const isLocal = server.url === 'http://localhost:' + servers[1].port
         const checkAttributes = {
@@ -1072,7 +1040,7 @@ describe('Test multiple servers', function () {
             }
           ]
         }
-        await completeVideoCheck(server.url, video, checkAttributes)
+        await completeVideoCheck(server, video, checkAttributes)
       }
     })
   })
index 4fc3317dfc9aa27317c8db651cb97e5e4bac7e37..857859fd3a95c4209bd361b052871caf6dea3d44 100644 (file)
@@ -4,22 +4,15 @@ import 'mocha'
 import * as chai from 'chai'
 import { pathExists, readdir, stat } from 'fs-extra'
 import { join } from 'path'
-import { HttpStatusCode } from '@shared/core-utils'
 import {
   buildAbsoluteFixturePath,
-  buildServerDirectory,
   cleanupTests,
-  flushAndRunServer,
-  getMyUserInformation,
-  prepareResumableUpload,
-  sendDebugCommand,
-  sendResumableChunks,
-  ServerInfo,
+  createSingleServer,
+  PeerTubeServer,
   setAccessTokensToServers,
-  setDefaultVideoChannel,
-  updateUser
+  setDefaultVideoChannel
 } from '@shared/extra-utils'
-import { MyUser, VideoPrivacy } from '@shared/models'
+import { HttpStatusCode, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
@@ -27,7 +20,7 @@ const expect = chai.expect
 
 describe('Test resumable upload', function () {
   const defaultFixture = 'video_short.mp4'
-  let server: ServerInfo
+  let server: PeerTubeServer
   let rootId: number
 
   async function buildSize (fixture: string, size?: number) {
@@ -42,14 +35,14 @@ describe('Test resumable upload', function () {
 
     const attributes = {
       name: 'video',
-      channelId: server.videoChannel.id,
+      channelId: server.store.channel.id,
       privacy: VideoPrivacy.PUBLIC,
       fixture: defaultFixture
     }
 
     const mimetype = 'video/mp4'
 
-    const res = await prepareResumableUpload({ url: server.url, token: server.accessToken, attributes, size, mimetype })
+    const res = await server.videos.prepareResumableUpload({ attributes, size, mimetype })
 
     return res.header['location'].split('?')[1]
   }
@@ -67,15 +60,13 @@ describe('Test resumable upload', function () {
     const size = await buildSize(defaultFixture, options.size)
     const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture)
 
-    return sendResumableChunks({
-      url: server.url,
-      token: server.accessToken,
+    return server.videos.sendResumableChunks({
       pathUploadId,
       videoFilePath: absoluteFilePath,
       size,
       contentLength,
       contentRangeBuilder,
-      specialStatus: expectedStatus
+      expectedStatus
     })
   }
 
@@ -83,7 +74,7 @@ describe('Test resumable upload', function () {
     const uploadId = uploadIdArg.replace(/^upload_id=/, '')
 
     const subPath = join('tmp', 'resumable-uploads', uploadId)
-    const filePath = buildServerDirectory(server, subPath)
+    const filePath = server.servers.buildDirectory(subPath)
     const exists = await pathExists(filePath)
 
     if (expectedSize === null) {
@@ -98,7 +89,7 @@ describe('Test resumable upload', function () {
 
   async function countResumableUploads () {
     const subPath = join('tmp', 'resumable-uploads')
-    const filePath = buildServerDirectory(server, subPath)
+    const filePath = server.servers.buildDirectory(subPath)
 
     const files = await readdir(filePath)
     return files.length
@@ -107,19 +98,14 @@ describe('Test resumable upload', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
     await setDefaultVideoChannel([ server ])
 
-    const res = await getMyUserInformation(server.url, server.accessToken)
-    rootId = (res.body as MyUser).id
+    const body = await server.users.getMyInfo()
+    rootId = body.id
 
-    await updateUser({
-      url: server.url,
-      userId: rootId,
-      accessToken: server.accessToken,
-      videoQuota: 10_000_000
-    })
+    await server.users.update({ userId: rootId, videoQuota: 10_000_000 })
   })
 
   describe('Directory cleaning', function () {
@@ -138,13 +124,13 @@ describe('Test resumable upload', function () {
     })
 
     it('Should not delete recent uploads', async function () {
-      await sendDebugCommand(server.url, server.accessToken, { command: 'remove-dandling-resumable-uploads' })
+      await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
 
       expect(await countResumableUploads()).to.equal(2)
     })
 
     it('Should delete old uploads', async function () {
-      await sendDebugCommand(server.url, server.accessToken, { command: 'remove-dandling-resumable-uploads' })
+      await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
 
       expect(await countResumableUploads()).to.equal(0)
     })
@@ -160,8 +146,7 @@ describe('Test resumable upload', function () {
     })
 
     it('Should not accept more chunks than expected', async function () {
-      const size = 100
-      const uploadId = await prepareUpload(size)
+      const uploadId = await prepareUpload(100)
 
       await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 })
       await checkFileSize(uploadId, 0)
@@ -170,8 +155,14 @@ describe('Test resumable upload', function () {
     it('Should not accept more chunks than expected with an invalid content length/content range', async function () {
       const uploadId = await prepareUpload(1500)
 
-      await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 })
-      await checkFileSize(uploadId, 0)
+      // Content length check seems to have changed in v16
+      if (process.version.startsWith('v16')) {
+        await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409, contentLength: 1000 })
+        await checkFileSize(uploadId, 1000)
+      } else {
+        await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 })
+        await checkFileSize(uploadId, 0)
+      }
     })
 
     it('Should not accept more chunks than expected with an invalid content length', async function () {
@@ -179,8 +170,13 @@ describe('Test resumable upload', function () {
 
       const size = 1000
 
-      const contentRangeBuilder = start => `bytes ${start}-${start + size - 1}/${size}`
-      await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentRangeBuilder, contentLength: size })
+      // Content length check seems to have changed in v16
+      const expectedStatus = process.version.startsWith('v16')
+        ? HttpStatusCode.CONFLICT_409
+        : HttpStatusCode.BAD_REQUEST_400
+
+      const contentRangeBuilder = (start: number) => `bytes ${start}-${start + size - 1}/${size}`
+      await sendChunks({ pathUploadId: uploadId, expectedStatus, contentRangeBuilder, contentLength: size })
       await checkFileSize(uploadId, 0)
     })
   })
index 1058a1e9c4fe46166ccee99d07dca7830f292d8c..29dac6ec1c6ddd64129aef1fabda4c177d212879 100644 (file)
@@ -2,43 +2,26 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { keyBy } from 'lodash'
-
 import {
   checkVideoFilesWereRemoved,
   cleanupTests,
   completeVideoCheck,
-  flushAndRunServer,
-  getVideo,
-  getVideoCategories,
-  getVideoLanguages,
-  getVideoLicences,
-  getVideoPrivacies,
-  getVideosList,
-  getVideosListPagination,
-  getVideosListSort,
-  getVideosWithFilters,
-  rateVideo,
-  removeVideo,
-  ServerInfo,
+  createSingleServer,
+  PeerTubeServer,
   setAccessTokensToServers,
   testImage,
-  updateVideo,
-  uploadVideo,
-  viewVideo,
   wait
-} from '../../../../shared/extra-utils'
-import { VideoPrivacy } from '../../../../shared/models/videos'
-import { HttpStatusCode } from '@shared/core-utils'
+} from '@shared/extra-utils'
+import { Video, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test a single server', function () {
 
   function runSuite (mode: 'legacy' | 'resumable') {
-    let server: ServerInfo = null
-    let videoId = -1
-    let videoId2 = -1
+    let server: PeerTubeServer = null
+    let videoId: number | string
+    let videoId2: string
     let videoUUID = ''
     let videosListBase: any[] = null
 
@@ -111,134 +94,123 @@ describe('Test a single server', function () {
     before(async function () {
       this.timeout(30000)
 
-      server = await flushAndRunServer(1)
+      server = await createSingleServer(1)
 
       await setAccessTokensToServers([ server ])
     })
 
     it('Should list video categories', async function () {
-      const res = await getVideoCategories(server.url)
-
-      const categories = res.body
+      const categories = await server.videos.getCategories()
       expect(Object.keys(categories)).to.have.length.above(10)
 
       expect(categories[11]).to.equal('News & Politics')
     })
 
     it('Should list video licences', async function () {
-      const res = await getVideoLicences(server.url)
-
-      const licences = res.body
+      const licences = await server.videos.getLicences()
       expect(Object.keys(licences)).to.have.length.above(5)
 
       expect(licences[3]).to.equal('Attribution - No Derivatives')
     })
 
     it('Should list video languages', async function () {
-      const res = await getVideoLanguages(server.url)
-
-      const languages = res.body
+      const languages = await server.videos.getLanguages()
       expect(Object.keys(languages)).to.have.length.above(5)
 
       expect(languages['ru']).to.equal('Russian')
     })
 
     it('Should list video privacies', async function () {
-      const res = await getVideoPrivacies(server.url)
-
-      const privacies = res.body
+      const privacies = await server.videos.getPrivacies()
       expect(Object.keys(privacies)).to.have.length.at.least(3)
 
       expect(privacies[3]).to.equal('Private')
     })
 
     it('Should not have videos', async function () {
-      const res = await getVideosList(server.url)
+      const { data, total } = await server.videos.list()
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data.length).to.equal(0)
+      expect(total).to.equal(0)
+      expect(data).to.be.an('array')
+      expect(data.length).to.equal(0)
     })
 
     it('Should upload the video', async function () {
       this.timeout(10000)
 
-      const videoAttributes = {
+      const attributes = {
         name: 'my super name',
         category: 2,
         nsfw: true,
         licence: 6,
         tags: [ 'tag1', 'tag2', 'tag3' ]
       }
-      const res = await uploadVideo(server.url, server.accessToken, videoAttributes, HttpStatusCode.OK_200, mode)
-      expect(res.body.video).to.not.be.undefined
-      expect(res.body.video.id).to.equal(1)
-      expect(res.body.video.uuid).to.have.length.above(5)
+      const video = await server.videos.upload({ attributes, mode })
+      expect(video).to.not.be.undefined
+      expect(video.id).to.equal(1)
+      expect(video.uuid).to.have.length.above(5)
 
-      videoId = res.body.video.id
-      videoUUID = res.body.video.uuid
+      videoId = video.id
+      videoUUID = video.uuid
     })
 
     it('Should get and seed the uploaded video', async function () {
       this.timeout(5000)
 
-      const res = await getVideosList(server.url)
+      const { data, total } = await server.videos.list()
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data.length).to.equal(1)
+      expect(total).to.equal(1)
+      expect(data).to.be.an('array')
+      expect(data.length).to.equal(1)
 
-      const video = res.body.data[0]
-      await completeVideoCheck(server.url, video, getCheckAttributes())
+      const video = data[0]
+      await completeVideoCheck(server, video, getCheckAttributes())
     })
 
     it('Should get the video by UUID', async function () {
       this.timeout(5000)
 
-      const res = await getVideo(server.url, videoUUID)
-
-      const video = res.body
-      await completeVideoCheck(server.url, video, getCheckAttributes())
+      const video = await server.videos.get({ id: videoUUID })
+      await completeVideoCheck(server, video, getCheckAttributes())
     })
 
     it('Should have the views updated', async function () {
       this.timeout(20000)
 
-      await viewVideo(server.url, videoId)
-      await viewVideo(server.url, videoId)
-      await viewVideo(server.url, videoId)
+      await server.videos.view({ id: videoId })
+      await server.videos.view({ id: videoId })
+      await server.videos.view({ id: videoId })
 
       await wait(1500)
 
-      await viewVideo(server.url, videoId)
-      await viewVideo(server.url, videoId)
+      await server.videos.view({ id: videoId })
+      await server.videos.view({ id: videoId })
 
       await wait(1500)
 
-      await viewVideo(server.url, videoId)
-      await viewVideo(server.url, videoId)
+      await server.videos.view({ id: videoId })
+      await server.videos.view({ id: videoId })
 
       // Wait the repeatable job
       await wait(8000)
 
-      const res = await getVideo(server.url, videoId)
-
-      const video = res.body
+      const video = await server.videos.get({ id: videoId })
       expect(video.views).to.equal(3)
     })
 
     it('Should remove the video', async function () {
-      await removeVideo(server.url, server.accessToken, videoId)
+      const video = await server.videos.get({ id: videoId })
+      await server.videos.remove({ id: videoId })
 
-      await checkVideoFilesWereRemoved(videoUUID, 1)
+      await checkVideoFilesWereRemoved({ video, server })
     })
 
     it('Should not have videos', async function () {
-      const res = await getVideosList(server.url)
+      const { total, data } = await server.videos.list()
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(total).to.equal(0)
+      expect(data).to.be.an('array')
+      expect(data).to.have.lengthOf(0)
     })
 
     it('Should upload 6 videos', async function () {
@@ -250,7 +222,7 @@ describe('Test a single server', function () {
       ])
 
       for (const video of videos) {
-        const videoAttributes = {
+        const attributes = {
           name: video + ' name',
           description: video + ' description',
           category: 2,
@@ -261,19 +233,20 @@ describe('Test a single server', function () {
           fixture: video
         }
 
-        await uploadVideo(server.url, server.accessToken, videoAttributes, HttpStatusCode.OK_200, mode)
+        await server.videos.upload({ attributes, mode })
       }
     })
 
     it('Should have the correct durations', async function () {
-      const res = await getVideosList(server.url)
+      const { total, data } = await server.videos.list()
+
+      expect(total).to.equal(6)
+      expect(data).to.be.an('array')
+      expect(data).to.have.lengthOf(6)
 
-      expect(res.body.total).to.equal(6)
-      const videos = res.body.data
-      expect(videos).to.be.an('array')
-      expect(videos).to.have.lengthOf(6)
+      const videosByName: { [ name: string ]: Video } = {}
+      data.forEach(v => { videosByName[v.name] = v })
 
-      const videosByName = keyBy<{ duration: number }>(videos, 'name')
       expect(videosByName['video_short.mp4 name'].duration).to.equal(5)
       expect(videosByName['video_short.ogv name'].duration).to.equal(5)
       expect(videosByName['video_short.webm name'].duration).to.equal(5)
@@ -283,96 +256,87 @@ describe('Test a single server', function () {
     })
 
     it('Should have the correct thumbnails', async function () {
-      const res = await getVideosList(server.url)
+      const { data } = await server.videos.list()
 
-      const videos = res.body.data
       // For the next test
-      videosListBase = videos
+      videosListBase = data
 
-      for (const video of videos) {
+      for (const video of data) {
         const videoName = video.name.replace(' name', '')
         await testImage(server.url, videoName, video.thumbnailPath)
       }
     })
 
     it('Should list only the two first videos', async function () {
-      const res = await getVideosListPagination(server.url, 0, 2, 'name')
+      const { total, data } = await server.videos.list({ start: 0, count: 2, sort: 'name' })
 
-      const videos = res.body.data
-      expect(res.body.total).to.equal(6)
-      expect(videos.length).to.equal(2)
-      expect(videos[0].name).to.equal(videosListBase[0].name)
-      expect(videos[1].name).to.equal(videosListBase[1].name)
+      expect(total).to.equal(6)
+      expect(data.length).to.equal(2)
+      expect(data[0].name).to.equal(videosListBase[0].name)
+      expect(data[1].name).to.equal(videosListBase[1].name)
     })
 
     it('Should list only the next three videos', async function () {
-      const res = await getVideosListPagination(server.url, 2, 3, 'name')
+      const { total, data } = await server.videos.list({ start: 2, count: 3, sort: 'name' })
 
-      const videos = res.body.data
-      expect(res.body.total).to.equal(6)
-      expect(videos.length).to.equal(3)
-      expect(videos[0].name).to.equal(videosListBase[2].name)
-      expect(videos[1].name).to.equal(videosListBase[3].name)
-      expect(videos[2].name).to.equal(videosListBase[4].name)
+      expect(total).to.equal(6)
+      expect(data.length).to.equal(3)
+      expect(data[0].name).to.equal(videosListBase[2].name)
+      expect(data[1].name).to.equal(videosListBase[3].name)
+      expect(data[2].name).to.equal(videosListBase[4].name)
     })
 
     it('Should list the last video', async function () {
-      const res = await getVideosListPagination(server.url, 5, 6, 'name')
+      const { total, data } = await server.videos.list({ start: 5, count: 6, sort: 'name' })
 
-      const videos = res.body.data
-      expect(res.body.total).to.equal(6)
-      expect(videos.length).to.equal(1)
-      expect(videos[0].name).to.equal(videosListBase[5].name)
+      expect(total).to.equal(6)
+      expect(data.length).to.equal(1)
+      expect(data[0].name).to.equal(videosListBase[5].name)
     })
 
     it('Should not have the total field', async function () {
-      const res = await getVideosListPagination(server.url, 5, 6, 'name', true)
+      const { total, data } = await server.videos.list({ start: 5, count: 6, sort: 'name', skipCount: true })
 
-      const videos = res.body.data
-      expect(res.body.total).to.not.exist
-      expect(videos.length).to.equal(1)
-      expect(videos[0].name).to.equal(videosListBase[5].name)
+      expect(total).to.not.exist
+      expect(data.length).to.equal(1)
+      expect(data[0].name).to.equal(videosListBase[5].name)
     })
 
     it('Should list and sort by name in descending order', async function () {
-      const res = await getVideosListSort(server.url, '-name')
+      const { total, data } = await server.videos.list({ sort: '-name' })
 
-      const videos = res.body.data
-      expect(res.body.total).to.equal(6)
-      expect(videos.length).to.equal(6)
-      expect(videos[0].name).to.equal('video_short.webm name')
-      expect(videos[1].name).to.equal('video_short.ogv name')
-      expect(videos[2].name).to.equal('video_short.mp4 name')
-      expect(videos[3].name).to.equal('video_short3.webm name')
-      expect(videos[4].name).to.equal('video_short2.webm name')
-      expect(videos[5].name).to.equal('video_short1.webm name')
+      expect(total).to.equal(6)
+      expect(data.length).to.equal(6)
+      expect(data[0].name).to.equal('video_short.webm name')
+      expect(data[1].name).to.equal('video_short.ogv name')
+      expect(data[2].name).to.equal('video_short.mp4 name')
+      expect(data[3].name).to.equal('video_short3.webm name')
+      expect(data[4].name).to.equal('video_short2.webm name')
+      expect(data[5].name).to.equal('video_short1.webm name')
 
-      videoId = videos[3].uuid
-      videoId2 = videos[5].uuid
+      videoId = data[3].uuid
+      videoId2 = data[5].uuid
     })
 
     it('Should list and sort by trending in descending order', async function () {
-      const res = await getVideosListPagination(server.url, 0, 2, '-trending')
+      const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-trending' })
 
-      const videos = res.body.data
-      expect(res.body.total).to.equal(6)
-      expect(videos.length).to.equal(2)
+      expect(total).to.equal(6)
+      expect(data.length).to.equal(2)
     })
 
     it('Should list and sort by hotness in descending order', async function () {
-      const res = await getVideosListPagination(server.url, 0, 2, '-hot')
+      const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-hot' })
 
-      const videos = res.body.data
-      expect(res.body.total).to.equal(6)
-      expect(videos.length).to.equal(2)
+      expect(total).to.equal(6)
+      expect(data.length).to.equal(2)
     })
 
     it('Should list and sort by best in descending order', async function () {
-      const res = await getVideosListPagination(server.url, 0, 2, '-best')
+      const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-best' })
 
-      const videos = res.body.data
-      expect(res.body.total).to.equal(6)
-      expect(videos.length).to.equal(2)
+      expect(total).to.equal(6)
+      expect(data.length).to.equal(2)
     })
 
     it('Should update a video', async function () {
@@ -387,67 +351,66 @@ describe('Test a single server', function () {
         downloadEnabled: false,
         tags: [ 'tagup1', 'tagup2' ]
       }
-      await updateVideo(server.url, server.accessToken, videoId, attributes)
+      await server.videos.update({ id: videoId, attributes })
     })
 
     it('Should filter by tags and category', async function () {
-      const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] })
-      expect(res1.body.total).to.equal(1)
-      expect(res1.body.data[0].name).to.equal('my super video updated')
+      {
+        const { data, total } = await server.videos.list({ tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] })
+        expect(total).to.equal(1)
+        expect(data[0].name).to.equal('my super video updated')
+      }
 
-      const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] })
-      expect(res2.body.total).to.equal(0)
+      {
+        const { total } = await server.videos.list({ tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] })
+        expect(total).to.equal(0)
+      }
     })
 
     it('Should have the video updated', async function () {
       this.timeout(60000)
 
-      const res = await getVideo(server.url, videoId)
-      const video = res.body
+      const video = await server.videos.get({ id: videoId })
 
-      await completeVideoCheck(server.url, video, updateCheckAttributes())
+      await completeVideoCheck(server, video, updateCheckAttributes())
     })
 
     it('Should update only the tags of a video', async function () {
       const attributes = {
         tags: [ 'supertag', 'tag1', 'tag2' ]
       }
-      await updateVideo(server.url, server.accessToken, videoId, attributes)
+      await server.videos.update({ id: videoId, attributes })
 
-      const res = await getVideo(server.url, videoId)
-      const video = res.body
+      const video = await server.videos.get({ id: videoId })
 
-      await completeVideoCheck(server.url, video, Object.assign(updateCheckAttributes(), attributes))
+      await completeVideoCheck(server, video, Object.assign(updateCheckAttributes(), attributes))
     })
 
     it('Should update only the description of a video', async function () {
       const attributes = {
         description: 'hello everybody'
       }
-      await updateVideo(server.url, server.accessToken, videoId, attributes)
+      await server.videos.update({ id: videoId, attributes })
 
-      const res = await getVideo(server.url, videoId)
-      const video = res.body
+      const video = await server.videos.get({ id: videoId })
 
       const expectedAttributes = Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes)
-      await completeVideoCheck(server.url, video, expectedAttributes)
+      await completeVideoCheck(server, video, expectedAttributes)
     })
 
     it('Should like a video', async function () {
-      await rateVideo(server.url, server.accessToken, videoId, 'like')
+      await server.videos.rate({ id: videoId, rating: 'like' })
 
-      const res = await getVideo(server.url, videoId)
-      const video = res.body
+      const video = await server.videos.get({ id: videoId })
 
       expect(video.likes).to.equal(1)
       expect(video.dislikes).to.equal(0)
     })
 
     it('Should dislike the same video', async function () {
-      await rateVideo(server.url, server.accessToken, videoId, 'dislike')
+      await server.videos.rate({ id: videoId, rating: 'dislike' })
 
-      const res = await getVideo(server.url, videoId)
-      const video = res.body
+      const video = await server.videos.get({ id: videoId })
 
       expect(video.likes).to.equal(0)
       expect(video.dislikes).to.equal(1)
@@ -457,10 +420,10 @@ describe('Test a single server', function () {
       {
         const now = new Date()
         const attributes = { originallyPublishedAt: now.toISOString() }
-        await updateVideo(server.url, server.accessToken, videoId, attributes)
+        await server.videos.update({ id: videoId, attributes })
 
-        const res = await getVideosListSort(server.url, '-originallyPublishedAt')
-        const names = res.body.data.map(v => v.name)
+        const { data } = await server.videos.list({ sort: '-originallyPublishedAt' })
+        const names = data.map(v => v.name)
 
         expect(names[0]).to.equal('my super video updated')
         expect(names[1]).to.equal('video_short2.webm name')
@@ -473,10 +436,10 @@ describe('Test a single server', function () {
       {
         const now = new Date()
         const attributes = { originallyPublishedAt: now.toISOString() }
-        await updateVideo(server.url, server.accessToken, videoId2, attributes)
+        await server.videos.update({ id: videoId2, attributes })
 
-        const res = await getVideosListSort(server.url, '-originallyPublishedAt')
-        const names = res.body.data.map(v => v.name)
+        const { data } = await server.videos.list({ sort: '-originallyPublishedAt' })
+        const names = data.map(v => v.name)
 
         expect(names[0]).to.equal('video_short1.webm name')
         expect(names[1]).to.equal('my super video updated')
index 14ecedfa6a6517fd42e9f13ce7750e1e76e9c984..3bb0d131cba6db5d9a8d01e990a97a7701f3e6b0 100644 (file)
@@ -1,72 +1,61 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import {
   checkVideoFilesWereRemoved,
   cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  removeVideo,
-  uploadVideo,
-  wait
-} from '../../../../shared/extra-utils'
-import { ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import {
-  createVideoCaption,
-  deleteVideoCaption,
-  listVideoCaptions,
-  testCaptionFile
-} from '../../../../shared/extra-utils/videos/video-captions'
-import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
+  PeerTubeServer,
+  setAccessTokensToServers,
+  testCaptionFile,
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test video captions', function () {
   const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
 
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let videoUUID: string
 
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
     await doubleFollow(servers[0], servers[1])
 
     await waitJobs(servers)
 
-    const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'my video name' })
-    videoUUID = res.body.video.uuid
+    const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video name' } })
+    videoUUID = uuid
 
     await waitJobs(servers)
   })
 
   it('Should list the captions and return an empty list', async function () {
     for (const server of servers) {
-      const res = await listVideoCaptions(server.url, videoUUID)
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.have.lengthOf(0)
+      const body = await server.captions.list({ videoId: videoUUID })
+      expect(body.total).to.equal(0)
+      expect(body.data).to.have.lengthOf(0)
     }
   })
 
   it('Should create two new captions', async function () {
     this.timeout(30000)
 
-    await createVideoCaption({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
+    await servers[0].captions.add({
       language: 'ar',
       videoId: videoUUID,
       fixture: 'subtitle-good1.vtt'
     })
 
-    await createVideoCaption({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
+    await servers[0].captions.add({
       language: 'zh',
       videoId: videoUUID,
       fixture: 'subtitle-good2.vtt',
@@ -78,17 +67,17 @@ describe('Test video captions', function () {
 
   it('Should list these uploaded captions', async function () {
     for (const server of servers) {
-      const res = await listVideoCaptions(server.url, videoUUID)
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.have.lengthOf(2)
+      const body = await server.captions.list({ videoId: videoUUID })
+      expect(body.total).to.equal(2)
+      expect(body.data).to.have.lengthOf(2)
 
-      const caption1: VideoCaption = res.body.data[0]
+      const caption1 = body.data[0]
       expect(caption1.language.id).to.equal('ar')
       expect(caption1.language.label).to.equal('Arabic')
       expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
       await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 1.')
 
-      const caption2: VideoCaption = res.body.data[1]
+      const caption2 = body.data[1]
       expect(caption2.language.id).to.equal('zh')
       expect(caption2.language.label).to.equal('Chinese')
       expect(caption2.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-zh.vtt$'))
@@ -99,9 +88,7 @@ describe('Test video captions', function () {
   it('Should replace an existing caption', async function () {
     this.timeout(30000)
 
-    await createVideoCaption({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
+    await servers[0].captions.add({
       language: 'ar',
       videoId: videoUUID,
       fixture: 'subtitle-good2.vtt'
@@ -112,11 +99,11 @@ describe('Test video captions', function () {
 
   it('Should have this caption updated', async function () {
     for (const server of servers) {
-      const res = await listVideoCaptions(server.url, videoUUID)
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.have.lengthOf(2)
+      const body = await server.captions.list({ videoId: videoUUID })
+      expect(body.total).to.equal(2)
+      expect(body.data).to.have.lengthOf(2)
 
-      const caption1: VideoCaption = res.body.data[0]
+      const caption1 = body.data[0]
       expect(caption1.language.id).to.equal('ar')
       expect(caption1.language.label).to.equal('Arabic')
       expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
@@ -127,9 +114,7 @@ describe('Test video captions', function () {
   it('Should replace an existing caption with a srt file and convert it', async function () {
     this.timeout(30000)
 
-    await createVideoCaption({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
+    await servers[0].captions.add({
       language: 'ar',
       videoId: videoUUID,
       fixture: 'subtitle-good.srt'
@@ -143,11 +128,11 @@ describe('Test video captions', function () {
 
   it('Should have this caption updated and converted', async function () {
     for (const server of servers) {
-      const res = await listVideoCaptions(server.url, videoUUID)
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.have.lengthOf(2)
+      const body = await server.captions.list({ videoId: videoUUID })
+      expect(body.total).to.equal(2)
+      expect(body.data).to.have.lengthOf(2)
 
-      const caption1: VideoCaption = res.body.data[0]
+      const caption1 = body.data[0]
       expect(caption1.language.id).to.equal('ar')
       expect(caption1.language.label).to.equal('Arabic')
       expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
@@ -172,18 +157,18 @@ describe('Test video captions', function () {
   it('Should remove one caption', async function () {
     this.timeout(30000)
 
-    await deleteVideoCaption(servers[0].url, servers[0].accessToken, videoUUID, 'ar')
+    await servers[0].captions.delete({ videoId: videoUUID, language: 'ar' })
 
     await waitJobs(servers)
   })
 
   it('Should only list the caption that was not deleted', async function () {
     for (const server of servers) {
-      const res = await listVideoCaptions(server.url, videoUUID)
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
+      const body = await server.captions.list({ videoId: videoUUID })
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
 
-      const caption: VideoCaption = res.body.data[0]
+      const caption = body.data[0]
 
       expect(caption.language.id).to.equal('zh')
       expect(caption.language.label).to.equal('Chinese')
@@ -193,9 +178,12 @@ describe('Test video captions', function () {
   })
 
   it('Should remove the video, and thus all video captions', async function () {
-    await removeVideo(servers[0].url, servers[0].accessToken, videoUUID)
+    const video = await servers[0].videos.get({ id: videoUUID })
+    const { data: captions } = await servers[0].captions.list({ videoId: videoUUID })
+
+    await servers[0].videos.remove({ id: videoUUID })
 
-    await checkVideoFilesWereRemoved(videoUUID, 1)
+    await checkVideoFilesWereRemoved({ server: servers[0], video, captions })
   })
 
   after(async function () {
index a3384851b72a2cf4c69448bcfd38de213cbc1f45..d6665fe4e89d5ad9a91b00f0e476ed7f5bb73bbf 100644 (file)
 
 import 'mocha'
 import * as chai from 'chai'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
-  acceptChangeOwnership,
-  changeVideoOwnership,
+  ChangeOwnershipCommand,
   cleanupTests,
-  createLive,
-  createUser,
+  createMultipleServers,
+  createSingleServer,
   doubleFollow,
-  flushAndRunMultipleServers,
-  flushAndRunServer,
-  getMyUserInformation,
-  getVideo,
-  getVideoChangeOwnershipList,
-  getVideosList,
-  refuseChangeOwnership,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
   setDefaultVideoChannel,
-  updateCustomSubConfig,
-  uploadVideo,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { User } from '../../../../shared/models/users'
-import { VideoDetails, VideoPrivacy } from '../../../../shared/models/videos'
+  waitJobs
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test video change ownership - nominal', function () {
-  let servers: ServerInfo[] = []
-  const firstUser = {
-    username: 'first',
-    password: 'My great password'
-  }
-  const secondUser = {
-    username: 'second',
-    password: 'My other password'
-  }
-
-  let firstUserAccessToken = ''
+  let servers: PeerTubeServer[] = []
+
+  const firstUser = 'first'
+  const secondUser = 'second'
+
+  let firstUserToken = ''
   let firstUserChannelId: number
 
-  let secondUserAccessToken = ''
+  let secondUserToken = ''
   let secondUserChannelId: number
 
-  let lastRequestChangeOwnershipId = ''
+  let lastRequestId: number
 
   let liveId: number
 
+  let command: ChangeOwnershipCommand
+
   before(async function () {
     this.timeout(50000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      transcoding: {
-        enabled: false
-      },
-      live: {
-        enabled: true
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        transcoding: {
+          enabled: false
+        },
+        live: {
+          enabled: true
+        }
       }
     })
 
-    const videoQuota = 42000000
-    await createUser({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      username: firstUser.username,
-      password: firstUser.password,
-      videoQuota: videoQuota
-    })
-    await createUser({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      username: secondUser.username,
-      password: secondUser.password,
-      videoQuota: videoQuota
-    })
-
-    firstUserAccessToken = await userLogin(servers[0], firstUser)
-    secondUserAccessToken = await userLogin(servers[0], secondUser)
+    firstUserToken = await servers[0].users.generateUserAndToken(firstUser)
+    secondUserToken = await servers[0].users.generateUserAndToken(secondUser)
 
     {
-      const res = await getMyUserInformation(servers[0].url, firstUserAccessToken)
-      const firstUserInformation: User = res.body
-      firstUserChannelId = firstUserInformation.videoChannels[0].id
+      const { videoChannels } = await servers[0].users.getMyInfo({ token: firstUserToken })
+      firstUserChannelId = videoChannels[0].id
     }
 
     {
-      const res = await getMyUserInformation(servers[0].url, secondUserAccessToken)
-      const secondUserInformation: User = res.body
-      secondUserChannelId = secondUserInformation.videoChannels[0].id
+      const { videoChannels } = await servers[0].users.getMyInfo({ token: secondUserToken })
+      secondUserChannelId = videoChannels[0].id
     }
 
     {
-      const videoAttributes = {
+      const attributes = {
         name: 'my super name',
         description: 'my super description'
       }
-      const res = await uploadVideo(servers[0].url, firstUserAccessToken, videoAttributes)
+      const { id } = await servers[0].videos.upload({ token: firstUserToken, attributes })
 
-      const resVideo = await getVideo(servers[0].url, res.body.video.id)
-      servers[0].video = resVideo.body
+      servers[0].store.videoCreated = await servers[0].videos.get({ id })
     }
 
     {
       const attributes = { name: 'live', channelId: firstUserChannelId, privacy: VideoPrivacy.PUBLIC }
-      const res = await createLive(servers[0].url, firstUserAccessToken, attributes)
+      const video = await servers[0].live.create({ token: firstUserToken, fields: attributes })
 
-      liveId = res.body.video.id
+      liveId = video.id
     }
 
+    command = servers[0].changeOwnership
+
     await doubleFollow(servers[0], servers[1])
   })
 
   it('Should not have video change ownership', async function () {
-    const resFirstUser = await getVideoChangeOwnershipList(servers[0].url, firstUserAccessToken)
+    {
+      const body = await command.list({ token: firstUserToken })
 
-    expect(resFirstUser.body.total).to.equal(0)
-    expect(resFirstUser.body.data).to.be.an('array')
-    expect(resFirstUser.body.data.length).to.equal(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data.length).to.equal(0)
+    }
 
-    const resSecondUser = await getVideoChangeOwnershipList(servers[0].url, secondUserAccessToken)
+    {
+      const body = await command.list({ token: secondUserToken })
 
-    expect(resSecondUser.body.total).to.equal(0)
-    expect(resSecondUser.body.data).to.be.an('array')
-    expect(resSecondUser.body.data.length).to.equal(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data.length).to.equal(0)
+    }
   })
 
   it('Should send a request to change ownership of a video', async function () {
     this.timeout(15000)
 
-    await changeVideoOwnership(servers[0].url, firstUserAccessToken, servers[0].video.id, secondUser.username)
+    await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser })
   })
 
   it('Should only return a request to change ownership for the second user', async function () {
-    const resFirstUser = await getVideoChangeOwnershipList(servers[0].url, firstUserAccessToken)
+    {
+      const body = await command.list({ token: firstUserToken })
 
-    expect(resFirstUser.body.total).to.equal(0)
-    expect(resFirstUser.body.data).to.be.an('array')
-    expect(resFirstUser.body.data.length).to.equal(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data.length).to.equal(0)
+    }
 
-    const resSecondUser = await getVideoChangeOwnershipList(servers[0].url, secondUserAccessToken)
+    {
+      const body = await command.list({ token: secondUserToken })
 
-    expect(resSecondUser.body.total).to.equal(1)
-    expect(resSecondUser.body.data).to.be.an('array')
-    expect(resSecondUser.body.data.length).to.equal(1)
+      expect(body.total).to.equal(1)
+      expect(body.data).to.be.an('array')
+      expect(body.data.length).to.equal(1)
 
-    lastRequestChangeOwnershipId = resSecondUser.body.data[0].id
+      lastRequestId = body.data[0].id
+    }
   })
 
   it('Should accept the same change ownership request without crashing', async function () {
     this.timeout(10000)
 
-    await changeVideoOwnership(servers[0].url, firstUserAccessToken, servers[0].video.id, secondUser.username)
+    await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser })
   })
 
   it('Should not create multiple change ownership requests while one is waiting', async function () {
     this.timeout(10000)
 
-    const resSecondUser = await getVideoChangeOwnershipList(servers[0].url, secondUserAccessToken)
+    const body = await command.list({ token: secondUserToken })
 
-    expect(resSecondUser.body.total).to.equal(1)
-    expect(resSecondUser.body.data).to.be.an('array')
-    expect(resSecondUser.body.data.length).to.equal(1)
+    expect(body.total).to.equal(1)
+    expect(body.data).to.be.an('array')
+    expect(body.data.length).to.equal(1)
   })
 
   it('Should not be possible to refuse the change of ownership from first user', async function () {
     this.timeout(10000)
 
-    await refuseChangeOwnership(servers[0].url, firstUserAccessToken, lastRequestChangeOwnershipId, HttpStatusCode.FORBIDDEN_403)
+    await command.refuse({ token: firstUserToken, ownershipId: lastRequestId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
   })
 
   it('Should be possible to refuse the change of ownership from second user', async function () {
     this.timeout(10000)
 
-    await refuseChangeOwnership(servers[0].url, secondUserAccessToken, lastRequestChangeOwnershipId)
+    await command.refuse({ token: secondUserToken, ownershipId: lastRequestId })
   })
 
   it('Should send a new request to change ownership of a video', async function () {
     this.timeout(15000)
 
-    await changeVideoOwnership(servers[0].url, firstUserAccessToken, servers[0].video.id, secondUser.username)
+    await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser })
   })
 
   it('Should return two requests to change ownership for the second user', async function () {
-    const resFirstUser = await getVideoChangeOwnershipList(servers[0].url, firstUserAccessToken)
+    {
+      const body = await command.list({ token: firstUserToken })
 
-    expect(resFirstUser.body.total).to.equal(0)
-    expect(resFirstUser.body.data).to.be.an('array')
-    expect(resFirstUser.body.data.length).to.equal(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data.length).to.equal(0)
+    }
 
-    const resSecondUser = await getVideoChangeOwnershipList(servers[0].url, secondUserAccessToken)
+    {
+      const body = await command.list({ token: secondUserToken })
 
-    expect(resSecondUser.body.total).to.equal(2)
-    expect(resSecondUser.body.data).to.be.an('array')
-    expect(resSecondUser.body.data.length).to.equal(2)
+      expect(body.total).to.equal(2)
+      expect(body.data).to.be.an('array')
+      expect(body.data.length).to.equal(2)
 
-    lastRequestChangeOwnershipId = resSecondUser.body.data[0].id
+      lastRequestId = body.data[0].id
+    }
   })
 
   it('Should not be possible to accept the change of ownership from first user', async function () {
     this.timeout(10000)
 
-    await acceptChangeOwnership(
-      servers[0].url,
-      firstUserAccessToken,
-      lastRequestChangeOwnershipId,
-      secondUserChannelId,
-      HttpStatusCode.FORBIDDEN_403
-    )
+    await command.accept({
+      token: firstUserToken,
+      ownershipId: lastRequestId,
+      channelId: secondUserChannelId,
+      expectedStatus: HttpStatusCode.FORBIDDEN_403
+    })
   })
 
   it('Should be possible to accept the change of ownership from second user', async function () {
     this.timeout(10000)
 
-    await acceptChangeOwnership(servers[0].url, secondUserAccessToken, lastRequestChangeOwnershipId, secondUserChannelId)
+    await command.accept({ token: secondUserToken, ownershipId: lastRequestId, channelId: secondUserChannelId })
 
     await waitJobs(servers)
   })
 
   it('Should have the channel of the video updated', async function () {
     for (const server of servers) {
-      const res = await getVideo(server.url, servers[0].video.uuid)
-
-      const video: VideoDetails = res.body
+      const video = await server.videos.get({ id: servers[0].store.videoCreated.uuid })
 
       expect(video.name).to.equal('my super name')
       expect(video.channel.displayName).to.equal('Main second channel')
@@ -240,27 +218,25 @@ describe('Test video change ownership - nominal', function () {
   it('Should send a request to change ownership of a live', async function () {
     this.timeout(15000)
 
-    await changeVideoOwnership(servers[0].url, firstUserAccessToken, liveId, secondUser.username)
+    await command.create({ token: firstUserToken, videoId: liveId, username: secondUser })
 
-    const resSecondUser = await getVideoChangeOwnershipList(servers[0].url, secondUserAccessToken)
+    const body = await command.list({ token: secondUserToken })
 
-    expect(resSecondUser.body.total).to.equal(3)
-    expect(resSecondUser.body.data.length).to.equal(3)
+    expect(body.total).to.equal(3)
+    expect(body.data.length).to.equal(3)
 
-    lastRequestChangeOwnershipId = resSecondUser.body.data[0].id
+    lastRequestId = body.data[0].id
   })
 
   it('Should accept a live ownership change', async function () {
     this.timeout(20000)
 
-    await acceptChangeOwnership(servers[0].url, secondUserAccessToken, lastRequestChangeOwnershipId, secondUserChannelId)
+    await command.accept({ token: secondUserToken, ownershipId: lastRequestId, channelId: secondUserChannelId })
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideo(server.url, servers[0].video.uuid)
-
-      const video: VideoDetails = res.body
+      const video = await server.videos.get({ id: servers[0].store.videoCreated.uuid })
 
       expect(video.name).to.equal('my super name')
       expect(video.channel.displayName).to.equal('Main second channel')
@@ -274,99 +250,79 @@ describe('Test video change ownership - nominal', function () {
 })
 
 describe('Test video change ownership - quota too small', function () {
-  let server: ServerInfo
-  const firstUser = {
-    username: 'first',
-    password: 'My great password'
-  }
-  const secondUser = {
-    username: 'second',
-    password: 'My other password'
-  }
-  let firstUserAccessToken = ''
-  let secondUserAccessToken = ''
-  let lastRequestChangeOwnershipId = ''
+  let server: PeerTubeServer
+  const firstUser = 'first'
+  const secondUser = 'second'
+
+  let firstUserToken = ''
+  let secondUserToken = ''
+  let lastRequestId: number
 
   before(async function () {
     this.timeout(50000)
 
     // Run one server
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
-    const videoQuota = 42000000
-    const limitedVideoQuota = 10
-    await createUser({
-      url: server.url,
-      accessToken: server.accessToken,
-      username: firstUser.username,
-      password: firstUser.password,
-      videoQuota: videoQuota
-    })
-    await createUser({
-      url: server.url,
-      accessToken: server.accessToken,
-      username: secondUser.username,
-      password: secondUser.password,
-      videoQuota: limitedVideoQuota
-    })
+    await server.users.create({ username: secondUser, videoQuota: 10 })
 
-    firstUserAccessToken = await userLogin(server, firstUser)
-    secondUserAccessToken = await userLogin(server, secondUser)
+    firstUserToken = await server.users.generateUserAndToken(firstUser)
+    secondUserToken = await server.login.getAccessToken(secondUser)
 
     // Upload some videos on the server
-    const video1Attributes = {
+    const attributes = {
       name: 'my super name',
       description: 'my super description'
     }
-    await uploadVideo(server.url, firstUserAccessToken, video1Attributes)
+    await server.videos.upload({ token: firstUserToken, attributes })
 
     await waitJobs(server)
 
-    const res = await getVideosList(server.url)
-    const videos = res.body.data
-
-    expect(videos.length).to.equal(1)
+    const { data } = await server.videos.list()
+    expect(data.length).to.equal(1)
 
-    server.video = videos.find(video => video.name === 'my super name')
+    server.store.videoCreated = data.find(video => video.name === 'my super name')
   })
 
   it('Should send a request to change ownership of a video', async function () {
     this.timeout(15000)
 
-    await changeVideoOwnership(server.url, firstUserAccessToken, server.video.id, secondUser.username)
+    await server.changeOwnership.create({ token: firstUserToken, videoId: server.store.videoCreated.id, username: secondUser })
   })
 
   it('Should only return a request to change ownership for the second user', async function () {
-    const resFirstUser = await getVideoChangeOwnershipList(server.url, firstUserAccessToken)
+    {
+      const body = await server.changeOwnership.list({ token: firstUserToken })
 
-    expect(resFirstUser.body.total).to.equal(0)
-    expect(resFirstUser.body.data).to.be.an('array')
-    expect(resFirstUser.body.data.length).to.equal(0)
+      expect(body.total).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data.length).to.equal(0)
+    }
 
-    const resSecondUser = await getVideoChangeOwnershipList(server.url, secondUserAccessToken)
+    {
+      const body = await server.changeOwnership.list({ token: secondUserToken })
 
-    expect(resSecondUser.body.total).to.equal(1)
-    expect(resSecondUser.body.data).to.be.an('array')
-    expect(resSecondUser.body.data.length).to.equal(1)
+      expect(body.total).to.equal(1)
+      expect(body.data).to.be.an('array')
+      expect(body.data.length).to.equal(1)
 
-    lastRequestChangeOwnershipId = resSecondUser.body.data[0].id
+      lastRequestId = body.data[0].id
+    }
   })
 
   it('Should not be possible to accept the change of ownership from second user because of exceeded quota', async function () {
     this.timeout(10000)
 
-    const secondUserInformationResponse = await getMyUserInformation(server.url, secondUserAccessToken)
-    const secondUserInformation: User = secondUserInformationResponse.body
-    const channelId = secondUserInformation.videoChannels[0].id
+    const { videoChannels } = await server.users.getMyInfo({ token: secondUserToken })
+    const channelId = videoChannels[0].id
 
-    await acceptChangeOwnership(
-      server.url,
-      secondUserAccessToken,
-      lastRequestChangeOwnershipId,
+    await server.changeOwnership.accept({
+      token: secondUserToken,
+      ownershipId: lastRequestId,
       channelId,
-      HttpStatusCode.PAYLOAD_TOO_LARGE_413
-    )
+      expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
+    })
   })
 
   after(async function () {
index 8650987777f77745ea7d1cbe1f02d3a478626eb8..c25754eb6a92d3c2a5f8c75e69705f5bd4b4ad82 100644 (file)
@@ -6,48 +6,28 @@ import { basename } from 'path'
 import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
 import {
   cleanupTests,
-  createUser,
-  deleteVideoChannelImage,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getActorImage,
-  getVideo,
-  getVideoChannel,
-  getVideoChannelVideos,
+  PeerTubeServer,
+  setAccessTokensToServers,
   setDefaultVideoChannel,
   testFileExistsOrNot,
   testImage,
-  updateVideo,
-  updateVideoChannelImage,
-  uploadVideo,
-  userLogin,
-  wait
-} from '../../../../shared/extra-utils'
-import {
-  addVideoChannel,
-  deleteVideoChannel,
-  getAccountVideoChannelsList,
-  getMyUserInformation,
-  getVideoChannelsList,
-  ServerInfo,
-  setAccessTokensToServers,
-  updateVideoChannel,
-  viewVideo
-} from '../../../../shared/extra-utils/index'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index'
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { User, VideoChannel } from '@shared/models'
 
 const expect = chai.expect
 
-async function findChannel (server: ServerInfo, channelId: number) {
-  const res = await getVideoChannelsList(server.url, 0, 5, '-name')
-  const videoChannel = res.body.data.find(c => c.id === channelId)
+async function findChannel (server: PeerTubeServer, channelId: number) {
+  const body = await server.channels.list({ sort: '-name' })
 
-  return videoChannel as VideoChannel
+  return body.data.find(c => c.id === channelId)
 }
 
 describe('Test video channels', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let userInfo: User
   let secondVideoChannelId: number
   let totoChannel: number
@@ -60,7 +40,7 @@ describe('Test video channels', function () {
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
@@ -69,11 +49,11 @@ describe('Test video channels', function () {
   })
 
   it('Should have one video channel (created with root)', async () => {
-    const res = await getVideoChannelsList(servers[0].url, 0, 2)
+    const body = await servers[0].channels.list({ start: 0, count: 2 })
 
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data).to.have.lengthOf(1)
+    expect(body.total).to.equal(1)
+    expect(body.data).to.be.an('array')
+    expect(body.data).to.have.lengthOf(1)
   })
 
   it('Should create another video channel', async function () {
@@ -86,23 +66,22 @@ describe('Test video channels', function () {
         description: 'super video channel description',
         support: 'super video channel support text'
       }
-      const res = await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel)
-      secondVideoChannelId = res.body.videoChannel.id
+      const created = await servers[0].channels.create({ attributes: videoChannel })
+      secondVideoChannelId = created.id
     }
 
     // The channel is 1 is propagated to servers 2
     {
-      const videoAttributesArg = { name: 'my video name', channelId: secondVideoChannelId, support: 'video support field' }
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributesArg)
-      videoUUID = res.body.video.uuid
+      const attributes = { name: 'my video name', channelId: secondVideoChannelId, support: 'video support field' }
+      const { uuid } = await servers[0].videos.upload({ attributes })
+      videoUUID = uuid
     }
 
     await waitJobs(servers)
   })
 
   it('Should have two video channels when getting my information', async () => {
-    const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
-    userInfo = res.body
+    userInfo = await servers[0].users.getMyInfo()
 
     expect(userInfo.videoChannels).to.be.an('array')
     expect(userInfo.videoChannels).to.have.lengthOf(2)
@@ -120,16 +99,14 @@ describe('Test video channels', function () {
   })
 
   it('Should have two video channels when getting account channels on server 1', async function () {
-    const res = await getAccountVideoChannelsList({
-      url: servers[0].url,
-      accountName
-    })
+    const body = await servers[0].channels.listByAccount({ accountName })
+    expect(body.total).to.equal(2)
+
+    const videoChannels = body.data
 
-    expect(res.body.total).to.equal(2)
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data).to.have.lengthOf(2)
+    expect(videoChannels).to.be.an('array')
+    expect(videoChannels).to.have.lengthOf(2)
 
-    const videoChannels = res.body.data
     expect(videoChannels[0].name).to.equal('root_channel')
     expect(videoChannels[0].displayName).to.equal('Main root channel')
 
@@ -141,79 +118,69 @@ describe('Test video channels', function () {
 
   it('Should paginate and sort account channels', async function () {
     {
-      const res = await getAccountVideoChannelsList({
-        url: servers[0].url,
+      const body = await servers[0].channels.listByAccount({
         accountName,
         start: 0,
         count: 1,
         sort: 'createdAt'
       })
 
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.have.lengthOf(1)
+      expect(body.total).to.equal(2)
+      expect(body.data).to.have.lengthOf(1)
 
-      const videoChannel: VideoChannel = res.body.data[0]
+      const videoChannel: VideoChannel = body.data[0]
       expect(videoChannel.name).to.equal('root_channel')
     }
 
     {
-      const res = await getAccountVideoChannelsList({
-        url: servers[0].url,
+      const body = await servers[0].channels.listByAccount({
         accountName,
         start: 0,
         count: 1,
         sort: '-createdAt'
       })
 
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.have.lengthOf(1)
-
-      const videoChannel: VideoChannel = res.body.data[0]
-      expect(videoChannel.name).to.equal('second_video_channel')
+      expect(body.total).to.equal(2)
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].name).to.equal('second_video_channel')
     }
 
     {
-      const res = await getAccountVideoChannelsList({
-        url: servers[0].url,
+      const body = await servers[0].channels.listByAccount({
         accountName,
         start: 1,
         count: 1,
         sort: '-createdAt'
       })
 
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.have.lengthOf(1)
-
-      const videoChannel: VideoChannel = res.body.data[0]
-      expect(videoChannel.name).to.equal('root_channel')
+      expect(body.total).to.equal(2)
+      expect(body.data).to.have.lengthOf(1)
+      expect(body.data[0].name).to.equal('root_channel')
     }
   })
 
   it('Should have one video channel when getting account channels on server 2', async function () {
-    const res = await getAccountVideoChannelsList({
-      url: servers[1].url,
-      accountName
-    })
+    const body = await servers[1].channels.listByAccount({ accountName })
 
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data).to.have.lengthOf(1)
+    expect(body.total).to.equal(1)
+    expect(body.data).to.be.an('array')
+    expect(body.data).to.have.lengthOf(1)
 
-    const videoChannels = res.body.data
-    expect(videoChannels[0].name).to.equal('second_video_channel')
-    expect(videoChannels[0].displayName).to.equal('second video channel')
-    expect(videoChannels[0].description).to.equal('super video channel description')
-    expect(videoChannels[0].support).to.equal('super video channel support text')
+    const videoChannel = body.data[0]
+    expect(videoChannel.name).to.equal('second_video_channel')
+    expect(videoChannel.displayName).to.equal('second video channel')
+    expect(videoChannel.description).to.equal('super video channel description')
+    expect(videoChannel.support).to.equal('super video channel support text')
   })
 
   it('Should list video channels', async function () {
-    const res = await getVideoChannelsList(servers[0].url, 1, 1, '-name')
+    const body = await servers[0].channels.list({ start: 1, count: 1, sort: '-name' })
 
-    expect(res.body.total).to.equal(2)
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data).to.have.lengthOf(1)
-    expect(res.body.data[0].name).to.equal('root_channel')
-    expect(res.body.data[0].displayName).to.equal('Main root channel')
+    expect(body.total).to.equal(2)
+    expect(body.data).to.be.an('array')
+    expect(body.data).to.have.lengthOf(1)
+    expect(body.data[0].name).to.equal('root_channel')
+    expect(body.data[0].displayName).to.equal('Main root channel')
   })
 
   it('Should update video channel', async function () {
@@ -225,30 +192,29 @@ describe('Test video channels', function () {
       support: 'support updated'
     }
 
-    await updateVideoChannel(servers[0].url, servers[0].accessToken, 'second_video_channel', videoChannelAttributes)
+    await servers[0].channels.update({ channelName: 'second_video_channel', attributes: videoChannelAttributes })
 
     await waitJobs(servers)
   })
 
   it('Should have video channel updated', async function () {
     for (const server of servers) {
-      const res = await getVideoChannelsList(server.url, 0, 1, '-name')
-
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].name).to.equal('second_video_channel')
-      expect(res.body.data[0].displayName).to.equal('video channel updated')
-      expect(res.body.data[0].description).to.equal('video channel description updated')
-      expect(res.body.data[0].support).to.equal('support updated')
+      const body = await server.channels.list({ start: 0, count: 1, sort: '-name' })
+
+      expect(body.total).to.equal(2)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(1)
+
+      expect(body.data[0].name).to.equal('second_video_channel')
+      expect(body.data[0].displayName).to.equal('video channel updated')
+      expect(body.data[0].description).to.equal('video channel description updated')
+      expect(body.data[0].support).to.equal('support updated')
     }
   })
 
   it('Should not have updated the video support field', async function () {
     for (const server of servers) {
-      const res = await getVideo(server.url, videoUUID)
-      const video: VideoDetails = res.body
-
+      const video = await server.videos.get({ id: videoUUID })
       expect(video.support).to.equal('video support field')
     }
   })
@@ -261,14 +227,12 @@ describe('Test video channels', function () {
       bulkVideosSupportUpdate: true
     }
 
-    await updateVideoChannel(servers[0].url, servers[0].accessToken, 'second_video_channel', videoChannelAttributes)
+    await servers[0].channels.update({ channelName: 'second_video_channel', attributes: videoChannelAttributes })
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideo(server.url, videoUUID)
-      const video: VideoDetails = res.body
-
+      const video = await server.videos.get({ id: videoUUID })
       expect(video.support).to.equal(videoChannelAttributes.support)
     }
   })
@@ -278,10 +242,8 @@ describe('Test video channels', function () {
 
     const fixture = 'avatar.png'
 
-    await updateVideoChannelImage({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      videoChannelName: 'second_video_channel',
+    await servers[0].channels.updateImage({
+      channelName: 'second_video_channel',
       fixture,
       type: 'avatar'
     })
@@ -295,7 +257,7 @@ describe('Test video channels', function () {
       await testImage(server.url, 'avatar-resized', avatarPaths[server.port], '.png')
       await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true)
 
-      const row = await getActorImage(server.internalServerNumber, basename(avatarPaths[server.port]))
+      const row = await server.sql.getActorImage(basename(avatarPaths[server.port]))
       expect(row.height).to.equal(ACTOR_IMAGES_SIZE.AVATARS.height)
       expect(row.width).to.equal(ACTOR_IMAGES_SIZE.AVATARS.width)
     }
@@ -306,10 +268,8 @@ describe('Test video channels', function () {
 
     const fixture = 'banner.jpg'
 
-    await updateVideoChannelImage({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      videoChannelName: 'second_video_channel',
+    await servers[0].channels.updateImage({
+      channelName: 'second_video_channel',
       fixture,
       type: 'banner'
     })
@@ -317,14 +277,13 @@ describe('Test video channels', function () {
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideoChannel(server.url, 'second_video_channel@' + servers[0].host)
-      const videoChannel = res.body
+      const videoChannel = await server.channels.get({ channelName: 'second_video_channel@' + servers[0].host })
 
       bannerPaths[server.port] = videoChannel.banner.path
       await testImage(server.url, 'banner-resized', bannerPaths[server.port])
       await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true)
 
-      const row = await getActorImage(server.internalServerNumber, basename(bannerPaths[server.port]))
+      const row = await server.sql.getActorImage(basename(bannerPaths[server.port]))
       expect(row.height).to.equal(ACTOR_IMAGES_SIZE.BANNERS.height)
       expect(row.width).to.equal(ACTOR_IMAGES_SIZE.BANNERS.width)
     }
@@ -333,12 +292,7 @@ describe('Test video channels', function () {
   it('Should delete the video channel avatar', async function () {
     this.timeout(15000)
 
-    await deleteVideoChannelImage({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      videoChannelName: 'second_video_channel',
-      type: 'avatar'
-    })
+    await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'avatar' })
 
     await waitJobs(servers)
 
@@ -353,12 +307,7 @@ describe('Test video channels', function () {
   it('Should delete the video channel banner', async function () {
     this.timeout(15000)
 
-    await deleteVideoChannelImage({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      videoChannelName: 'second_video_channel',
-      type: 'banner'
-    })
+    await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'banner' })
 
     await waitJobs(servers)
 
@@ -375,18 +324,19 @@ describe('Test video channels', function () {
 
     for (const server of servers) {
       const channelURI = 'second_video_channel@localhost:' + servers[0].port
-      const res1 = await getVideoChannelVideos(server.url, server.accessToken, channelURI, 0, 5)
-      expect(res1.body.total).to.equal(1)
-      expect(res1.body.data).to.be.an('array')
-      expect(res1.body.data).to.have.lengthOf(1)
-      expect(res1.body.data[0].name).to.equal('my video name')
+      const { total, data } = await server.videos.listByChannel({ handle: channelURI })
+
+      expect(total).to.equal(1)
+      expect(data).to.be.an('array')
+      expect(data).to.have.lengthOf(1)
+      expect(data[0].name).to.equal('my video name')
     }
   })
 
   it('Should change the video channel of a video', async function () {
     this.timeout(10000)
 
-    await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { channelId: servers[0].videoChannel.id })
+    await servers[0].videos.update({ id: videoUUID, attributes: { channelId: servers[0].store.channel.id } })
 
     await waitJobs(servers)
   })
@@ -395,47 +345,50 @@ describe('Test video channels', function () {
     this.timeout(10000)
 
     for (const server of servers) {
-      const secondChannelURI = 'second_video_channel@localhost:' + servers[0].port
-      const res1 = await getVideoChannelVideos(server.url, server.accessToken, secondChannelURI, 0, 5)
-      expect(res1.body.total).to.equal(0)
-
-      const channelURI = 'root_channel@localhost:' + servers[0].port
-      const res2 = await getVideoChannelVideos(server.url, server.accessToken, channelURI, 0, 5)
-      expect(res2.body.total).to.equal(1)
-
-      const videos: Video[] = res2.body.data
-      expect(videos).to.be.an('array')
-      expect(videos).to.have.lengthOf(1)
-      expect(videos[0].name).to.equal('my video name')
+      {
+        const secondChannelURI = 'second_video_channel@localhost:' + servers[0].port
+        const { total } = await server.videos.listByChannel({ handle: secondChannelURI })
+        expect(total).to.equal(0)
+      }
+
+      {
+        const channelURI = 'root_channel@localhost:' + servers[0].port
+        const { total, data } = await server.videos.listByChannel({ handle: channelURI })
+        expect(total).to.equal(1)
+
+        expect(data).to.be.an('array')
+        expect(data).to.have.lengthOf(1)
+        expect(data[0].name).to.equal('my video name')
+      }
     }
   })
 
   it('Should delete video channel', async function () {
-    await deleteVideoChannel(servers[0].url, servers[0].accessToken, 'second_video_channel')
+    await servers[0].channels.delete({ channelName: 'second_video_channel' })
   })
 
   it('Should have video channel deleted', async function () {
-    const res = await getVideoChannelsList(servers[0].url, 0, 10)
+    const body = await servers[0].channels.list({ start: 0, count: 10 })
 
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data).to.have.lengthOf(1)
-    expect(res.body.data[0].displayName).to.equal('Main root channel')
+    expect(body.total).to.equal(1)
+    expect(body.data).to.be.an('array')
+    expect(body.data).to.have.lengthOf(1)
+    expect(body.data[0].displayName).to.equal('Main root channel')
   })
 
   it('Should create the main channel with an uuid if there is a conflict', async function () {
     {
       const videoChannel = { name: 'toto_channel', displayName: 'My toto channel' }
-      const res = await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel)
-      totoChannel = res.body.videoChannel.id
+      const created = await servers[0].channels.create({ attributes: videoChannel })
+      totoChannel = created.id
     }
 
     {
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: 'toto', password: 'password' })
-      const accessToken = await userLogin(servers[0], { username: 'toto', password: 'password' })
+      await servers[0].users.create({ username: 'toto', password: 'password' })
+      const accessToken = await servers[0].login.getAccessToken({ username: 'toto', password: 'password' })
 
-      const res = await getMyUserInformation(servers[0].url, accessToken)
-      const videoChannel = res.body.videoChannels[0]
+      const { videoChannels } = await servers[0].users.getMyInfo({ token: accessToken })
+      const videoChannel = videoChannels[0]
       expect(videoChannel.name).to.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
     }
   })
@@ -444,15 +397,9 @@ describe('Test video channels', function () {
     this.timeout(10000)
 
     {
-      const res = await getAccountVideoChannelsList({
-        url: servers[0].url,
-        accountName,
-        withStats: true
-      })
-
-      const channels: VideoChannel[] = res.body.data
+      const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
 
-      for (const channel of channels) {
+      for (const channel of data) {
         expect(channel).to.haveOwnProperty('viewsPerDay')
         expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today
 
@@ -464,33 +411,24 @@ describe('Test video channels', function () {
     }
 
     {
-      // video has been posted on channel servers[0].videoChannel.id since last update
-      await viewVideo(servers[0].url, videoUUID, 204, '0.0.0.1,127.0.0.1')
-      await viewVideo(servers[0].url, videoUUID, 204, '0.0.0.2,127.0.0.1')
+      // video has been posted on channel servers[0].store.videoChannel.id since last update
+      await servers[0].videos.view({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1' })
+      await servers[0].videos.view({ id: videoUUID, xForwardedFor: '0.0.0.2,127.0.0.1' })
 
       // Wait the repeatable job
       await wait(8000)
 
-      const res = await getAccountVideoChannelsList({
-        url: servers[0].url,
-        accountName,
-        withStats: true
-      })
-      const channelWithView = res.body.data.find((channel: VideoChannel) => channel.id === servers[0].videoChannel.id)
+      const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
+      const channelWithView = data.find(channel => channel.id === servers[0].store.channel.id)
       expect(channelWithView.viewsPerDay.slice(-1)[0].views).to.equal(2)
     }
   })
 
   it('Should report correct videos count', async function () {
-    const res = await getAccountVideoChannelsList({
-      url: servers[0].url,
-      accountName,
-      withStats: true
-    })
-    const channels: VideoChannel[] = res.body.data
+    const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
 
-    const totoChannel = channels.find(c => c.name === 'toto_channel')
-    const rootChannel = channels.find(c => c.name === 'root_channel')
+    const totoChannel = data.find(c => c.name === 'toto_channel')
+    const rootChannel = data.find(c => c.name === 'root_channel')
 
     expect(rootChannel.videosCount).to.equal(1)
     expect(totoChannel.videosCount).to.equal(0)
@@ -498,26 +436,18 @@ describe('Test video channels', function () {
 
   it('Should search among account video channels', async function () {
     {
-      const res = await getAccountVideoChannelsList({
-        url: servers[0].url,
-        accountName,
-        search: 'root'
-      })
-      expect(res.body.total).to.equal(1)
+      const body = await servers[0].channels.listByAccount({ accountName, search: 'root' })
+      expect(body.total).to.equal(1)
 
-      const channels = res.body.data
+      const channels = body.data
       expect(channels).to.have.lengthOf(1)
     }
 
     {
-      const res = await getAccountVideoChannelsList({
-        url: servers[0].url,
-        accountName,
-        search: 'does not exist'
-      })
-      expect(res.body.total).to.equal(0)
+      const body = await servers[0].channels.listByAccount({ accountName, search: 'does not exist' })
+      expect(body.total).to.equal(0)
 
-      const channels = res.body.data
+      const channels = body.data
       expect(channels).to.have.lengthOf(0)
     }
   })
@@ -525,34 +455,24 @@ describe('Test video channels', function () {
   it('Should list channels by updatedAt desc if a video has been uploaded', async function () {
     this.timeout(30000)
 
-    await uploadVideo(servers[0].url, servers[0].accessToken, { channelId: totoChannel })
+    await servers[0].videos.upload({ attributes: { channelId: totoChannel } })
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getAccountVideoChannelsList({
-        url: server.url,
-        accountName,
-        sort: '-updatedAt'
-      })
+      const { data } = await server.channels.listByAccount({ accountName, sort: '-updatedAt' })
 
-      const channels: VideoChannel[] = res.body.data
-      expect(channels[0].name).to.equal('toto_channel')
-      expect(channels[1].name).to.equal('root_channel')
+      expect(data[0].name).to.equal('toto_channel')
+      expect(data[1].name).to.equal('root_channel')
     }
 
-    await uploadVideo(servers[0].url, servers[0].accessToken, { channelId: servers[0].videoChannel.id })
+    await servers[0].videos.upload({ attributes: { channelId: servers[0].store.channel.id } })
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getAccountVideoChannelsList({
-        url: server.url,
-        accountName,
-        sort: '-updatedAt'
-      })
+      const { data } = await server.channels.listByAccount({ accountName, sort: '-updatedAt' })
 
-      const channels: VideoChannel[] = res.body.data
-      expect(channels[0].name).to.equal('root_channel')
-      expect(channels[1].name).to.equal('toto_channel')
+      expect(data[0].name).to.equal('root_channel')
+      expect(data[1].name).to.equal('toto_channel')
     }
   })
 
index b6b00230726aa3e419b6472acd05aa6028f770b8..61ee54540963426fd27c92d3ec5298fe9caa9b57 100644 (file)
@@ -2,80 +2,62 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { VideoComment, VideoCommentAdmin, VideoCommentThreadTree } from '@shared/models'
-import { cleanupTests, testImage } from '../../../../shared/extra-utils'
 import {
-  createUser,
+  cleanupTests,
+  CommentsCommand,
+  createSingleServer,
   dateIsValid,
-  flushAndRunServer,
-  getAccessToken,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updateMyAvatar,
-  uploadVideo
-} from '../../../../shared/extra-utils/index'
-import {
-  addVideoCommentReply,
-  addVideoCommentThread,
-  deleteVideoComment,
-  getAdminVideoComments,
-  getVideoCommentThreads,
-  getVideoThreadComments
-} from '../../../../shared/extra-utils/videos/video-comments'
+  testImage
+} from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test video comments', function () {
-  let server: ServerInfo
-  let videoId
-  let videoUUID
-  let threadId
+  let server: PeerTubeServer
+  let videoId: number
+  let videoUUID: string
+  let threadId: number
   let replyToDeleteId: number
 
   let userAccessTokenServer1: string
 
+  let command: CommentsCommand
+
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
-    const res = await uploadVideo(server.url, server.accessToken, {})
-    videoUUID = res.body.video.uuid
-    videoId = res.body.video.id
+    const { id, uuid } = await server.videos.upload()
+    videoUUID = uuid
+    videoId = id
 
-    await updateMyAvatar({
-      url: server.url,
-      accessToken: server.accessToken,
-      fixture: 'avatar.png'
-    })
+    await server.users.updateMyAvatar({ fixture: 'avatar.png' })
 
-    await createUser({
-      url: server.url,
-      accessToken: server.accessToken,
-      username: 'user1',
-      password: 'password'
-    })
-    userAccessTokenServer1 = await getAccessToken(server.url, 'user1', 'password')
+    userAccessTokenServer1 = await server.users.generateUserAndToken('user1')
+
+    command = server.comments
   })
 
   describe('User comments', function () {
 
     it('Should not have threads on this video', async function () {
-      const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
+      const body = await command.listThreads({ videoId: videoUUID })
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.totalNotDeletedComments).to.equal(0)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(body.total).to.equal(0)
+      expect(body.totalNotDeletedComments).to.equal(0)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(0)
     })
 
     it('Should create a thread in this video', async function () {
       const text = 'my super first comment'
 
-      const res = await addVideoCommentThread(server.url, server.accessToken, videoUUID, text)
-      const comment = res.body.comment
+      const comment = await command.createThread({ videoId: videoUUID, text })
 
       expect(comment.inReplyToCommentId).to.be.null
       expect(comment.text).equal('my super first comment')
@@ -91,14 +73,14 @@ describe('Test video comments', function () {
     })
 
     it('Should list threads of this video', async function () {
-      const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
+      const body = await command.listThreads({ videoId: videoUUID })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.totalNotDeletedComments).to.equal(1)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(1)
+      expect(body.total).to.equal(1)
+      expect(body.totalNotDeletedComments).to.equal(1)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(1)
 
-      const comment: VideoComment = res.body.data[0]
+      const comment = body.data[0]
       expect(comment.inReplyToCommentId).to.be.null
       expect(comment.text).equal('my super first comment')
       expect(comment.videoId).to.equal(videoId)
@@ -117,9 +99,9 @@ describe('Test video comments', function () {
     })
 
     it('Should get all the thread created', async function () {
-      const res = await getVideoThreadComments(server.url, videoUUID, threadId)
+      const body = await command.getThread({ videoId: videoUUID, threadId })
 
-      const rootComment = res.body.comment
+      const rootComment = body.comment
       expect(rootComment.inReplyToCommentId).to.be.null
       expect(rootComment.text).equal('my super first comment')
       expect(rootComment.videoId).to.equal(videoId)
@@ -129,20 +111,19 @@ describe('Test video comments', function () {
 
     it('Should create multiple replies in this thread', async function () {
       const text1 = 'my super answer to thread 1'
-      const childCommentRes = await addVideoCommentReply(server.url, server.accessToken, videoId, threadId, text1)
-      const childCommentId = childCommentRes.body.comment.id
+      const created = await command.addReply({ videoId, toCommentId: threadId, text: text1 })
+      const childCommentId = created.id
 
       const text2 = 'my super answer to answer of thread 1'
-      await addVideoCommentReply(server.url, server.accessToken, videoId, childCommentId, text2)
+      await command.addReply({ videoId, toCommentId: childCommentId, text: text2 })
 
       const text3 = 'my second answer to thread 1'
-      await addVideoCommentReply(server.url, server.accessToken, videoId, threadId, text3)
+      await command.addReply({ videoId, toCommentId: threadId, text: text3 })
     })
 
     it('Should get correctly the replies', async function () {
-      const res = await getVideoThreadComments(server.url, videoUUID, threadId)
+      const tree = await command.getThread({ videoId: videoUUID, threadId })
 
-      const tree: VideoCommentThreadTree = res.body
       expect(tree.comment.text).equal('my super first comment')
       expect(tree.children).to.have.lengthOf(2)
 
@@ -163,42 +144,41 @@ describe('Test video comments', function () {
 
     it('Should create other threads', async function () {
       const text1 = 'super thread 2'
-      await addVideoCommentThread(server.url, server.accessToken, videoUUID, text1)
+      await command.createThread({ videoId: videoUUID, text: text1 })
 
       const text2 = 'super thread 3'
-      await addVideoCommentThread(server.url, server.accessToken, videoUUID, text2)
+      await command.createThread({ videoId: videoUUID, text: text2 })
     })
 
     it('Should list the threads', async function () {
-      const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt')
-
-      expect(res.body.total).to.equal(3)
-      expect(res.body.totalNotDeletedComments).to.equal(6)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(3)
-
-      expect(res.body.data[0].text).to.equal('my super first comment')
-      expect(res.body.data[0].totalReplies).to.equal(3)
-      expect(res.body.data[1].text).to.equal('super thread 2')
-      expect(res.body.data[1].totalReplies).to.equal(0)
-      expect(res.body.data[2].text).to.equal('super thread 3')
-      expect(res.body.data[2].totalReplies).to.equal(0)
+      const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' })
+
+      expect(body.total).to.equal(3)
+      expect(body.totalNotDeletedComments).to.equal(6)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(3)
+
+      expect(body.data[0].text).to.equal('my super first comment')
+      expect(body.data[0].totalReplies).to.equal(3)
+      expect(body.data[1].text).to.equal('super thread 2')
+      expect(body.data[1].totalReplies).to.equal(0)
+      expect(body.data[2].text).to.equal('super thread 3')
+      expect(body.data[2].totalReplies).to.equal(0)
     })
 
     it('Should delete a reply', async function () {
-      await deleteVideoComment(server.url, server.accessToken, videoId, replyToDeleteId)
+      await command.delete({ videoId, commentId: replyToDeleteId })
 
       {
-        const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt')
+        const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' })
 
-        expect(res.body.total).to.equal(3)
-        expect(res.body.totalNotDeletedComments).to.equal(5)
+        expect(body.total).to.equal(3)
+        expect(body.totalNotDeletedComments).to.equal(5)
       }
 
       {
-        const res = await getVideoThreadComments(server.url, videoUUID, threadId)
+        const tree = await command.getThread({ videoId: videoUUID, threadId })
 
-        const tree: VideoCommentThreadTree = res.body
         expect(tree.comment.text).equal('my super first comment')
         expect(tree.children).to.have.lengthOf(2)
 
@@ -220,99 +200,88 @@ describe('Test video comments', function () {
     })
 
     it('Should delete a complete thread', async function () {
-      await deleteVideoComment(server.url, server.accessToken, videoId, threadId)
-
-      const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt')
-      expect(res.body.total).to.equal(3)
-      expect(res.body.data).to.be.an('array')
-      expect(res.body.data).to.have.lengthOf(3)
-
-      expect(res.body.data[0].text).to.equal('')
-      expect(res.body.data[0].isDeleted).to.be.true
-      expect(res.body.data[0].deletedAt).to.not.be.null
-      expect(res.body.data[0].account).to.be.null
-      expect(res.body.data[0].totalReplies).to.equal(2)
-      expect(res.body.data[1].text).to.equal('super thread 2')
-      expect(res.body.data[1].totalReplies).to.equal(0)
-      expect(res.body.data[2].text).to.equal('super thread 3')
-      expect(res.body.data[2].totalReplies).to.equal(0)
+      await command.delete({ videoId, commentId: threadId })
+
+      const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' })
+      expect(body.total).to.equal(3)
+      expect(body.data).to.be.an('array')
+      expect(body.data).to.have.lengthOf(3)
+
+      expect(body.data[0].text).to.equal('')
+      expect(body.data[0].isDeleted).to.be.true
+      expect(body.data[0].deletedAt).to.not.be.null
+      expect(body.data[0].account).to.be.null
+      expect(body.data[0].totalReplies).to.equal(2)
+      expect(body.data[1].text).to.equal('super thread 2')
+      expect(body.data[1].totalReplies).to.equal(0)
+      expect(body.data[2].text).to.equal('super thread 3')
+      expect(body.data[2].totalReplies).to.equal(0)
     })
 
     it('Should count replies from the video author correctly', async function () {
-      const text = 'my super first comment'
-      await addVideoCommentThread(server.url, server.accessToken, videoUUID, text)
-      let res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
-      const comment: VideoComment = res.body.data[0]
-      const threadId2 = comment.threadId
+      await command.createThread({ videoId: videoUUID, text: 'my super first comment' })
+
+      const { data } = await command.listThreads({ videoId: videoUUID })
+      const threadId2 = data[0].threadId
 
       const text2 = 'a first answer to thread 4 by a third party'
-      await addVideoCommentReply(server.url, userAccessTokenServer1, videoId, threadId2, text2)
+      await command.addReply({ token: userAccessTokenServer1, videoId, toCommentId: threadId2, text: text2 })
 
       const text3 = 'my second answer to thread 4'
-      await addVideoCommentReply(server.url, server.accessToken, videoId, threadId2, text3)
+      await command.addReply({ videoId, toCommentId: threadId2, text: text3 })
 
-      res = await getVideoThreadComments(server.url, videoUUID, threadId2)
-      const tree: VideoCommentThreadTree = res.body
+      const tree = await command.getThread({ videoId: videoUUID, threadId: threadId2 })
       expect(tree.comment.totalReplies).to.equal(tree.comment.totalRepliesFromVideoAuthor + 1)
     })
   })
 
   describe('All instance comments', function () {
-    async function getComments (options: any = {}) {
-      const res = await getAdminVideoComments(Object.assign({
-        url: server.url,
-        token: server.accessToken,
-        start: 0,
-        count: 10
-      }, options))
-
-      return { comments: res.body.data as VideoCommentAdmin[], total: res.body.total as number }
-    }
 
     it('Should list instance comments as admin', async function () {
-      const { comments } = await getComments({ start: 0, count: 1 })
+      const { data } = await command.listForAdmin({ start: 0, count: 1 })
 
-      expect(comments[0].text).to.equal('my second answer to thread 4')
+      expect(data[0].text).to.equal('my second answer to thread 4')
     })
 
     it('Should filter instance comments by isLocal', async function () {
-      const { total, comments } = await getComments({ isLocal: false })
+      const { total, data } = await command.listForAdmin({ isLocal: false })
 
-      expect(comments).to.have.lengthOf(0)
+      expect(data).to.have.lengthOf(0)
       expect(total).to.equal(0)
     })
 
     it('Should search instance comments by account', async function () {
-      const { total, comments } = await getComments({ searchAccount: 'user' })
+      const { total, data } = await command.listForAdmin({ searchAccount: 'user' })
 
-      expect(comments).to.have.lengthOf(1)
+      expect(data).to.have.lengthOf(1)
       expect(total).to.equal(1)
 
-      expect(comments[0].text).to.equal('a first answer to thread 4 by a third party')
+      expect(data[0].text).to.equal('a first answer to thread 4 by a third party')
     })
 
     it('Should search instance comments by video', async function () {
       {
-        const { total, comments } = await getComments({ searchVideo: 'video' })
+        const { total, data } = await command.listForAdmin({ searchVideo: 'video' })
 
-        expect(comments).to.have.lengthOf(7)
+        expect(data).to.have.lengthOf(7)
         expect(total).to.equal(7)
       }
 
       {
-        const { total, comments } = await getComments({ searchVideo: 'hello' })
+        const { total, data } = await command.listForAdmin({ searchVideo: 'hello' })
 
-        expect(comments).to.have.lengthOf(0)
+        expect(data).to.have.lengthOf(0)
         expect(total).to.equal(0)
       }
     })
 
     it('Should search instance comments', async function () {
-      const { total, comments } = await getComments({ search: 'super thread 3' })
+      const { total, data } = await command.listForAdmin({ search: 'super thread 3' })
 
-      expect(comments).to.have.lengthOf(1)
       expect(total).to.equal(1)
-      expect(comments[0].text).to.equal('super thread 3')
+
+      expect(data).to.have.lengthOf(1)
+      expect(data[0].text).to.equal('super thread 3')
     })
   })
 
index b8e98e45f1cf64ce1c87717c89558c5cdfe97880..d22b4ed962370414982f1ae51d6820a026f3bc44 100644 (file)
@@ -1,25 +1,13 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import {
-  cleanupTests,
-  flushAndRunMultipleServers,
-  getVideo,
-  getVideoDescription,
-  getVideosList,
-  ServerInfo,
-  setAccessTokensToServers,
-  updateVideo,
-  uploadVideo
-} from '../../../../shared/extra-utils/index'
-import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
+import * as chai from 'chai'
+import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test video description', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let videoUUID = ''
   let videoId: number
   const longDescription = 'my super description for server 1'.repeat(50)
@@ -28,7 +16,7 @@ describe('Test video description', function () {
     this.timeout(40000)
 
     // Run servers
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -43,20 +31,19 @@ describe('Test video description', function () {
     const attributes = {
       description: longDescription
     }
-    await uploadVideo(servers[0].url, servers[0].accessToken, attributes)
+    await servers[0].videos.upload({ attributes })
 
     await waitJobs(servers)
 
-    const res = await getVideosList(servers[0].url)
+    const { data } = await servers[0].videos.list()
 
-    videoId = res.body.data[0].id
-    videoUUID = res.body.data[0].uuid
+    videoId = data[0].id
+    videoUUID = data[0].uuid
   })
 
   it('Should have a truncated description on each server', async function () {
     for (const server of servers) {
-      const res = await getVideo(server.url, videoUUID)
-      const video = res.body
+      const video = await server.videos.get({ id: videoUUID })
 
       // 30 characters * 6 -> 240 characters
       const truncatedDescription = 'my super description for server 1'.repeat(7) +
@@ -68,11 +55,10 @@ describe('Test video description', function () {
 
   it('Should fetch long description on each server', async function () {
     for (const server of servers) {
-      const res = await getVideo(server.url, videoUUID)
-      const video = res.body
+      const video = await server.videos.get({ id: videoUUID })
 
-      const res2 = await getVideoDescription(server.url, video.descriptionPath)
-      expect(res2.body.description).to.equal(longDescription)
+      const { description } = await server.videos.getDescription({ descriptionPath: video.descriptionPath })
+      expect(description).to.equal(longDescription)
     }
   })
 
@@ -82,20 +68,19 @@ describe('Test video description', function () {
     const attributes = {
       description: 'short description'
     }
-    await updateVideo(servers[0].url, servers[0].accessToken, videoId, attributes)
+    await servers[0].videos.update({ id: videoId, attributes })
 
     await waitJobs(servers)
   })
 
   it('Should have a small description on each server', async function () {
     for (const server of servers) {
-      const res = await getVideo(server.url, videoUUID)
-      const video = res.body
+      const video = await server.videos.get({ id: videoUUID })
 
       expect(video.description).to.equal('short description')
 
-      const res2 = await getVideoDescription(server.url, video.descriptionPath)
-      expect(res2.body.description).to.equal('short description')
+      const { description } = await server.videos.getDescription({ descriptionPath: video.descriptionPath })
+      expect(description).to.equal('short description')
     }
   })
 
index 03ac3f32166ec146dcae63a21a85a49cce205906..961f0e617fc0faf74e86cd7e9f18f1c54e924ddd 100644 (file)
@@ -2,38 +2,30 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { join } from 'path'
+import { basename, join } from 'path'
+import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils'
 import {
   checkDirectoryIsEmpty,
   checkResolutionsInMasterPlaylist,
   checkSegmentHash,
   checkTmpIsEmpty,
   cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getPlaylist,
-  getVideo,
   makeRawRequest,
-  removeVideo,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updateCustomSubConfig,
-  updateVideo,
-  uploadVideo,
   waitJobs,
   webtorrentAdd
-} from '../../../../shared/extra-utils'
-import { VideoDetails } from '../../../../shared/models/videos'
-import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoStreamingPlaylistType } from '@shared/models'
 import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const expect = chai.expect
 
-async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOnly: boolean, resolutions = [ 240, 360, 480, 720 ]) {
+async function checkHlsPlaylist (servers: PeerTubeServer[], videoUUID: string, hlsOnly: boolean, resolutions = [ 240, 360, 480, 720 ]) {
   for (const server of servers) {
-    const resVideoDetails = await getVideo(server.url, videoUUID)
-    const videoDetails: VideoDetails = resVideoDetails.body
+    const videoDetails = await server.videos.get({ id: videoUUID })
     const baseUrl = `http://${videoDetails.account.host}`
 
     expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
@@ -47,14 +39,17 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
     if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0)
     else expect(videoDetails.files).to.have.lengthOf(resolutions.length)
 
+    // Check JSON files
     for (const resolution of resolutions) {
       const file = hlsFiles.find(f => f.resolution.id === resolution)
       expect(file).to.not.be.undefined
 
       expect(file.magnetUri).to.have.lengthOf.above(2)
-      expect(file.torrentUrl).to.equal(`http://${server.host}/lazy-static/torrents/${videoDetails.uuid}-${file.resolution.id}-hls.torrent`)
-      expect(file.fileUrl).to.equal(
-        `${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${videoDetails.uuid}-${file.resolution.id}-fragmented.mp4`
+      expect(file.torrentUrl).to.match(
+        new RegExp(`http://${server.host}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}-hls.torrent`)
+      )
+      expect(file.fileUrl).to.match(
+        new RegExp(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${uuidRegex}-${file.resolution.id}-fragmented.mp4`)
       )
       expect(file.resolution.label).to.equal(resolution + 'p')
 
@@ -67,11 +62,11 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
       expect(torrent.files[0].path).to.exist.and.to.not.equal('')
     }
 
+    // Check master playlist
     {
-      await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions)
+      await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
 
-      const res = await getPlaylist(hlsPlaylist.playlistUrl)
-      const masterPlaylist = res.text
+      const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl })
 
       for (const resolution of resolutions) {
         expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
@@ -79,12 +74,18 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
       }
     }
 
+    // Check resolution playlists
     {
       for (const resolution of resolutions) {
-        const res = await getPlaylist(`${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`)
+        const file = hlsFiles.find(f => f.resolution.id === resolution)
+        const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8'
+
+        const subPlaylist = await server.streamingPlaylists.get({
+          url: `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${playlistName}`
+        })
 
-        const subPlaylist = res.text
-        expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
+        expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`))
+        expect(subPlaylist).to.contain(basename(file.fileUrl))
       }
     }
 
@@ -92,23 +93,31 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
       const baseUrlAndPath = baseUrl + '/static/streaming-playlists/hls'
 
       for (const resolution of resolutions) {
-        await checkSegmentHash(baseUrlAndPath, baseUrlAndPath, videoUUID, resolution, hlsPlaylist)
+        await checkSegmentHash({
+          server,
+          baseUrlPlaylist: baseUrlAndPath,
+          baseUrlSegment: baseUrlAndPath,
+          videoUUID,
+          resolution,
+          hlsPlaylist
+        })
       }
     }
   }
 }
 
 describe('Test HLS videos', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let videoUUID = ''
   let videoAudioUUID = ''
 
   function runTestSuite (hlsOnly: boolean) {
+
     it('Should upload a video and transcode it to HLS', async function () {
       this.timeout(120000)
 
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 1', fixture: 'video_short.webm' })
-      videoUUID = res.body.video.uuid
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1', fixture: 'video_short.webm' } })
+      videoUUID = uuid
 
       await waitJobs(servers)
 
@@ -118,8 +127,8 @@ describe('Test HLS videos', function () {
     it('Should upload an audio file and transcode it to HLS', async function () {
       this.timeout(120000)
 
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video audio', fixture: 'sample.ogg' })
-      videoAudioUUID = res.body.video.uuid
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video audio', fixture: 'sample.ogg' } })
+      videoAudioUUID = uuid
 
       await waitJobs(servers)
 
@@ -129,7 +138,7 @@ describe('Test HLS videos', function () {
     it('Should update the video', async function () {
       this.timeout(10000)
 
-      await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' })
+      await servers[0].videos.update({ id: videoUUID, attributes: { name: 'video 1 updated' } })
 
       await waitJobs(servers)
 
@@ -139,14 +148,14 @@ describe('Test HLS videos', function () {
     it('Should delete videos', async function () {
       this.timeout(10000)
 
-      await removeVideo(servers[0].url, servers[0].accessToken, videoUUID)
-      await removeVideo(servers[0].url, servers[0].accessToken, videoAudioUUID)
+      await servers[0].videos.remove({ id: videoUUID })
+      await servers[0].videos.remove({ id: videoAudioUUID })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        await getVideo(server.url, videoUUID, HttpStatusCode.NOT_FOUND_404)
-        await getVideo(server.url, videoAudioUUID, HttpStatusCode.NOT_FOUND_404)
+        await server.videos.get({ id: videoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+        await server.videos.get({ id: videoAudioUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
       }
     })
 
@@ -176,7 +185,7 @@ describe('Test HLS videos', function () {
         }
       }
     }
-    servers = await flushAndRunMultipleServers(2, configOverride)
+    servers = await createMultipleServers(2, configOverride)
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -192,24 +201,26 @@ describe('Test HLS videos', function () {
   describe('With only HLS enabled', function () {
 
     before(async function () {
-      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-        transcoding: {
-          enabled: true,
-          allowAudioFiles: true,
-          resolutions: {
-            '240p': true,
-            '360p': true,
-            '480p': true,
-            '720p': true,
-            '1080p': true,
-            '1440p': true,
-            '2160p': true
-          },
-          hls: {
-            enabled: true
-          },
-          webtorrent: {
-            enabled: false
+      await servers[0].config.updateCustomSubConfig({
+        newConfig: {
+          transcoding: {
+            enabled: true,
+            allowAudioFiles: true,
+            resolutions: {
+              '240p': true,
+              '360p': true,
+              '480p': true,
+              '720p': true,
+              '1080p': true,
+              '1440p': true,
+              '2160p': true
+            },
+            hls: {
+              enabled: true
+            },
+            webtorrent: {
+              enabled: false
+            }
           }
         }
       })
index 80834ca86ccc7c560ff58c343d06ffba31fd8093..2eac130d2a638186c2d44b872c4d2a3c6f0c76c3 100644 (file)
@@ -3,43 +3,30 @@
 import 'mocha'
 import * as chai from 'chai'
 import {
+  areHttpImportTestsDisabled,
   cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getMyUserInformation,
-  getMyVideos,
-  getVideo,
-  getVideosList,
-  immutableAssign,
-  listVideoCaptions,
-  ServerInfo,
+  FIXTURE_URLS,
+  PeerTubeServer,
   setAccessTokensToServers,
   testCaptionFile,
-  updateCustomSubConfig
-} from '../../../../shared/extra-utils'
-import { areHttpImportTestsDisabled, testImage } from '../../../../shared/extra-utils/miscs/miscs'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import {
-  getMagnetURI,
-  getMyVideoImports,
-  getYoutubeHDRVideoUrl,
-  getYoutubeVideoUrl,
-  importVideo
-} from '../../../../shared/extra-utils/videos/video-imports'
-import { VideoCaption, VideoDetails, VideoImport, VideoPrivacy, VideoResolution } from '../../../../shared/models/videos'
+  testImage,
+  waitJobs
+} from '@shared/extra-utils'
+import { VideoPrivacy, VideoResolution } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test video imports', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let channelIdServer1: number
   let channelIdServer2: number
 
   if (areHttpImportTestsDisabled()) return
 
-  async function checkVideosServer1 (url: string, idHttp: string, idMagnet: string, idTorrent: string) {
-    const resHttp = await getVideo(url, idHttp)
-    const videoHttp: VideoDetails = resHttp.body
+  async function checkVideosServer1 (server: PeerTubeServer, idHttp: string, idMagnet: string, idTorrent: string) {
+    const videoHttp = await server.videos.get({ id: idHttp })
 
     expect(videoHttp.name).to.equal('small video - youtube')
     // FIXME: youtube-dl seems broken
@@ -56,10 +43,8 @@ describe('Test video imports', function () {
     expect(originallyPublishedAt.getMonth()).to.equal(0)
     expect(originallyPublishedAt.getFullYear()).to.equal(2019)
 
-    const resMagnet = await getVideo(url, idMagnet)
-    const videoMagnet: VideoDetails = resMagnet.body
-    const resTorrent = await getVideo(url, idTorrent)
-    const videoTorrent: VideoDetails = resTorrent.body
+    const videoMagnet = await server.videos.get({ id: idMagnet })
+    const videoTorrent = await server.videos.get({ id: idTorrent })
 
     for (const video of [ videoMagnet, videoTorrent ]) {
       expect(video.category.label).to.equal('Misc')
@@ -74,13 +59,12 @@ describe('Test video imports', function () {
     expect(videoTorrent.name).to.contain('你好 世界 720p.mp4')
     expect(videoMagnet.name).to.contain('super peertube2 video')
 
-    const resCaptions = await listVideoCaptions(url, idHttp)
-    expect(resCaptions.body.total).to.equal(2)
+    const bodyCaptions = await server.captions.list({ videoId: idHttp })
+    expect(bodyCaptions.total).to.equal(2)
   }
 
-  async function checkVideoServer2 (url: string, id: number | string) {
-    const res = await getVideo(url, id)
-    const video: VideoDetails = res.body
+  async function checkVideoServer2 (server: PeerTubeServer, id: number | string) {
+    const video = await server.videos.get({ id })
 
     expect(video.name).to.equal('my super name')
     expect(video.category.label).to.equal('Entertainment')
@@ -92,26 +76,26 @@ describe('Test video imports', function () {
 
     expect(video.files).to.have.lengthOf(1)
 
-    const resCaptions = await listVideoCaptions(url, id)
-    expect(resCaptions.body.total).to.equal(2)
+    const bodyCaptions = await server.captions.list({ videoId: id })
+    expect(bodyCaptions.total).to.equal(2)
   }
 
   before(async function () {
     this.timeout(30_000)
 
     // Run servers
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
 
     {
-      const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
-      channelIdServer1 = res.body.videoChannels[0].id
+      const { videoChannels } = await servers[0].users.getMyInfo()
+      channelIdServer1 = videoChannels[0].id
     }
 
     {
-      const res = await getMyUserInformation(servers[1].url, servers[1].accessToken)
-      channelIdServer2 = res.body.videoChannels[0].id
+      const { videoChannels } = await servers[1].users.getMyInfo()
+      channelIdServer2 = videoChannels[0].id
     }
 
     await doubleFollow(servers[0], servers[1])
@@ -126,18 +110,18 @@ describe('Test video imports', function () {
     }
 
     {
-      const attributes = immutableAssign(baseAttributes, { targetUrl: getYoutubeVideoUrl() })
-      const res = await importVideo(servers[0].url, servers[0].accessToken, attributes)
-      expect(res.body.video.name).to.equal('small video - youtube')
+      const attributes = { ...baseAttributes, targetUrl: FIXTURE_URLS.youtube }
+      const { video } = await servers[0].imports.importVideo({ attributes })
+      expect(video.name).to.equal('small video - youtube')
 
-      expect(res.body.video.thumbnailPath).to.match(new RegExp(`^/static/thumbnails/.+.jpg$`))
-      expect(res.body.video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`))
+      expect(video.thumbnailPath).to.match(new RegExp(`^/static/thumbnails/.+.jpg$`))
+      expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`))
 
-      await testImage(servers[0].url, 'video_import_thumbnail', res.body.video.thumbnailPath)
-      await testImage(servers[0].url, 'video_import_preview', res.body.video.previewPath)
+      await testImage(servers[0].url, 'video_import_thumbnail', video.thumbnailPath)
+      await testImage(servers[0].url, 'video_import_preview', video.previewPath)
 
-      const resCaptions = await listVideoCaptions(servers[0].url, res.body.video.id)
-      const videoCaptions: VideoCaption[] = resCaptions.body.data
+      const bodyCaptions = await servers[0].captions.list({ videoId: video.id })
+      const videoCaptions = bodyCaptions.data
       expect(videoCaptions).to.have.lengthOf(2)
 
       const enCaption = videoCaptions.find(caption => caption.language.id === 'en')
@@ -176,52 +160,52 @@ Ajouter un sous-titre est vraiment facile`)
     }
 
     {
-      const attributes = immutableAssign(baseAttributes, {
-        magnetUri: getMagnetURI(),
+      const attributes = {
+        ...baseAttributes,
+        magnetUri: FIXTURE_URLS.magnet,
         description: 'this is a super torrent description',
         tags: [ 'tag_torrent1', 'tag_torrent2' ]
-      })
-      const res = await importVideo(servers[0].url, servers[0].accessToken, attributes)
-      expect(res.body.video.name).to.equal('super peertube2 video')
+      }
+      const { video } = await servers[0].imports.importVideo({ attributes })
+      expect(video.name).to.equal('super peertube2 video')
     }
 
     {
-      const attributes = immutableAssign(baseAttributes, {
+      const attributes = {
+        ...baseAttributes,
         torrentfile: 'video-720p.torrent' as any,
         description: 'this is a super torrent description',
         tags: [ 'tag_torrent1', 'tag_torrent2' ]
-      })
-      const res = await importVideo(servers[0].url, servers[0].accessToken, attributes)
-      expect(res.body.video.name).to.equal('你好 世界 720p.mp4')
+      }
+      const { video } = await servers[0].imports.importVideo({ attributes })
+      expect(video.name).to.equal('你好 世界 720p.mp4')
     }
   })
 
   it('Should list the videos to import in my videos on server 1', async function () {
-    const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5, 'createdAt')
+    const { total, data } = await servers[0].videos.listMyVideos({ sort: 'createdAt' })
 
-    expect(res.body.total).to.equal(3)
+    expect(total).to.equal(3)
 
-    const videos = res.body.data
-    expect(videos).to.have.lengthOf(3)
-    expect(videos[0].name).to.equal('small video - youtube')
-    expect(videos[1].name).to.equal('super peertube2 video')
-    expect(videos[2].name).to.equal('你好 世界 720p.mp4')
+    expect(data).to.have.lengthOf(3)
+    expect(data[0].name).to.equal('small video - youtube')
+    expect(data[1].name).to.equal('super peertube2 video')
+    expect(data[2].name).to.equal('你好 世界 720p.mp4')
   })
 
   it('Should list the videos to import in my imports on server 1', async function () {
-    const res = await getMyVideoImports(servers[0].url, servers[0].accessToken, '-createdAt')
+    const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ sort: '-createdAt' })
+    expect(total).to.equal(3)
 
-    expect(res.body.total).to.equal(3)
-    const videoImports: VideoImport[] = res.body.data
     expect(videoImports).to.have.lengthOf(3)
 
-    expect(videoImports[2].targetUrl).to.equal(getYoutubeVideoUrl())
+    expect(videoImports[2].targetUrl).to.equal(FIXTURE_URLS.youtube)
     expect(videoImports[2].magnetUri).to.be.null
     expect(videoImports[2].torrentName).to.be.null
     expect(videoImports[2].video.name).to.equal('small video - youtube')
 
     expect(videoImports[1].targetUrl).to.be.null
-    expect(videoImports[1].magnetUri).to.equal(getMagnetURI())
+    expect(videoImports[1].magnetUri).to.equal(FIXTURE_URLS.magnet)
     expect(videoImports[1].torrentName).to.be.null
     expect(videoImports[1].video.name).to.equal('super peertube2 video')
 
@@ -237,12 +221,12 @@ Ajouter un sous-titre est vraiment facile`)
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-      expect(res.body.total).to.equal(3)
-      expect(res.body.data).to.have.lengthOf(3)
+      const { total, data } = await server.videos.list()
+      expect(total).to.equal(3)
+      expect(data).to.have.lengthOf(3)
 
-      const [ videoHttp, videoMagnet, videoTorrent ] = res.body.data
-      await checkVideosServer1(server.url, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
+      const [ videoHttp, videoMagnet, videoTorrent ] = data
+      await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
     }
   })
 
@@ -250,7 +234,7 @@ Ajouter un sous-titre est vraiment facile`)
     this.timeout(60_000)
 
     const attributes = {
-      targetUrl: getYoutubeVideoUrl(),
+      targetUrl: FIXTURE_URLS.youtube,
       channelId: channelIdServer2,
       privacy: VideoPrivacy.PUBLIC,
       category: 10,
@@ -260,8 +244,8 @@ Ajouter un sous-titre est vraiment facile`)
       description: 'my super description',
       tags: [ 'supertag1', 'supertag2' ]
     }
-    const res = await importVideo(servers[1].url, servers[1].accessToken, attributes)
-    expect(res.body.video.name).to.equal('my super name')
+    const { video } = await servers[1].imports.importVideo({ attributes })
+    expect(video.name).to.equal('my super name')
   })
 
   it('Should have the videos listed on the two instances', async function () {
@@ -270,14 +254,14 @@ Ajouter un sous-titre est vraiment facile`)
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-      expect(res.body.total).to.equal(4)
-      expect(res.body.data).to.have.lengthOf(4)
+      const { total, data } = await server.videos.list()
+      expect(total).to.equal(4)
+      expect(data).to.have.lengthOf(4)
 
-      await checkVideoServer2(server.url, res.body.data[0].uuid)
+      await checkVideoServer2(serverdata[0].uuid)
 
-      const [ , videoHttp, videoMagnet, videoTorrent ] = res.body.data
-      await checkVideosServer1(server.url, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
+      const [ , videoHttp, videoMagnet, videoTorrent ] = data
+      await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
     }
   })
 
@@ -286,18 +270,17 @@ Ajouter un sous-titre est vraiment facile`)
 
     const attributes = {
       name: 'transcoded video',
-      magnetUri: getMagnetURI(),
+      magnetUri: FIXTURE_URLS.magnet,
       channelId: channelIdServer2,
       privacy: VideoPrivacy.PUBLIC
     }
-    const res = await importVideo(servers[1].url, servers[1].accessToken, attributes)
-    const videoUUID = res.body.video.uuid
+    const { video } = await servers[1].imports.importVideo({ attributes })
+    const videoUUID = video.uuid
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideo(server.url, videoUUID)
-      const video: VideoDetails = res.body
+      const video = await server.videos.get({ id: videoUUID })
 
       expect(video.name).to.equal('transcoded video')
       expect(video.files).to.have.lengthOf(4)
@@ -333,22 +316,21 @@ Ajouter un sous-titre est vraiment facile`)
         }
       }
     }
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+    await servers[0].config.updateCustomSubConfig({ newConfig: config })
 
     const attributes = {
       name: 'hdr video',
-      targetUrl: getYoutubeHDRVideoUrl(),
+      targetUrl: FIXTURE_URLS.youtubeHDR,
       channelId: channelIdServer1,
       privacy: VideoPrivacy.PUBLIC
     }
-    const res1 = await importVideo(servers[0].url, servers[0].accessToken, attributes)
-    const videoUUID = res1.body.video.uuid
+    const { video: videoImported } = await servers[0].imports.importVideo({ attributes })
+    const videoUUID = videoImported.uuid
 
     await waitJobs(servers)
 
     // test resolution
-    const res2 = await getVideo(servers[0].url, videoUUID)
-    const video: VideoDetails = res2.body
+    const video = await servers[0].videos.get({ id: videoUUID })
     expect(video.name).to.equal('hdr video')
     const maxResolution = Math.max.apply(Math, video.files.map(function (o) { return o.resolution.id }))
     expect(maxResolution, 'expected max resolution not met').to.equals(VideoResolution.H_1080P)
index b16b484b917ab10af0afe0c802a3b2ac438e5649..b5d183d6278deabca0acea74c4390a238d2846e0 100644 (file)
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { cleanupTests, getVideosList, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../../../shared/extra-utils/index'
-import { userLogin } from '../../../../shared/extra-utils/users/login'
-import { createUser } from '../../../../shared/extra-utils/users/users'
-import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
-import {
-  flushAndRunServer,
-  getAccountVideos,
-  getConfig,
-  getCustomConfig,
-  getMyUserInformation,
-  getVideoChannelVideos,
-  getVideosListWithToken,
-  searchVideo,
-  searchVideoWithToken,
-  updateCustomConfig,
-  updateMyUser
-} from '../../../../shared/extra-utils'
-import { ServerConfig, VideosOverview } from '../../../../shared/models'
-import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
-import { User } from '../../../../shared/models/users'
-import { getVideosOverview, getVideosOverviewWithToken } from '@shared/extra-utils/overviews/overviews'
+import * as chai from 'chai'
+import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/extra-utils'
+import { BooleanBothQuery, CustomConfig, ResultList, Video, VideosOverview } from '@shared/models'
 
 const expect = chai.expect
 
-function createOverviewRes (res: any) {
-  const overview = res.body as VideosOverview
-
+function createOverviewRes (overview: VideosOverview) {
   const videos = overview.categories[0].videos
-  return { body: { data: videos, total: videos.length } }
+  return { data: videos, total: videos.length }
 }
 
 describe('Test video NSFW policy', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken: string
   let customConfig: CustomConfig
 
-  function getVideosFunctions (token?: string, query = {}) {
-    return getMyUserInformation(server.url, server.accessToken)
-      .then(res => {
-        const user: User = res.body
-        const videoChannelName = user.videoChannels[0].name
-        const accountName = user.account.name + '@' + user.account.host
-        const hasQuery = Object.keys(query).length !== 0
-        let promises: Promise<any>[]
-
-        if (token) {
-          promises = [
-            getVideosListWithToken(server.url, token, query),
-            searchVideoWithToken(server.url, 'n', token, query),
-            getAccountVideos(server.url, token, accountName, 0, 5, undefined, query),
-            getVideoChannelVideos(server.url, token, videoChannelName, 0, 5, undefined, query)
-          ]
-
-          // Overviews do not support video filters
-          if (!hasQuery) {
-            promises.push(getVideosOverviewWithToken(server.url, 1, token).then(res => createOverviewRes(res)))
-          }
-
-          return Promise.all(promises)
-        }
-
-        promises = [
-          getVideosList(server.url),
-          searchVideo(server.url, 'n'),
-          getAccountVideos(server.url, undefined, accountName, 0, 5),
-          getVideoChannelVideos(server.url, undefined, videoChannelName, 0, 5)
-        ]
-
-        // Overviews do not support video filters
-        if (!hasQuery) {
-          promises.push(getVideosOverview(server.url, 1).then(res => createOverviewRes(res)))
-        }
-
-        return Promise.all(promises)
-      })
+  async function getVideosFunctions (token?: string, query: { nsfw?: BooleanBothQuery } = {}) {
+    const user = await server.users.getMyInfo()
+
+    const channelName = user.videoChannels[0].name
+    const accountName = user.account.name + '@' + user.account.host
+
+    const hasQuery = Object.keys(query).length !== 0
+    let promises: Promise<ResultList<Video>>[]
+
+    if (token) {
+      promises = [
+        server.search.advancedVideoSearch({ token, search: { search: 'n', sort: '-publishedAt', ...query } }),
+        server.videos.listWithToken({ token, ...query }),
+        server.videos.listByAccount({ token, handle: accountName, ...query }),
+        server.videos.listByChannel({ token, handle: channelName, ...query })
+      ]
+
+      // Overviews do not support video filters
+      if (!hasQuery) {
+        const p = server.overviews.getVideos({ page: 1, token })
+                                         .then(res => createOverviewRes(res))
+        promises.push(p)
+      }
+
+      return Promise.all(promises)
+    }
+
+    promises = [
+      server.search.searchVideos({ search: 'n', sort: '-publishedAt' }),
+      server.videos.list(),
+      server.videos.listByAccount({ token: null, handle: accountName }),
+      server.videos.listByChannel({ token: null, handle: channelName })
+    ]
+
+    // Overviews do not support video filters
+    if (!hasQuery) {
+      const p = server.overviews.getVideos({ page: 1 })
+                                       .then(res => createOverviewRes(res))
+      promises.push(p)
+    }
+
+    return Promise.all(promises)
   }
 
   before(async function () {
     this.timeout(50000)
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     // Get the access tokens
     await setAccessTokensToServers([ server ])
 
     {
       const attributes = { name: 'nsfw', nsfw: true, category: 1 }
-      await uploadVideo(server.url, server.accessToken, attributes)
+      await server.videos.upload({ attributes })
     }
 
     {
       const attributes = { name: 'normal', nsfw: false, category: 1 }
-      await uploadVideo(server.url, server.accessToken, attributes)
+      await server.videos.upload({ attributes })
     }
 
-    {
-      const res = await getCustomConfig(server.url, server.accessToken)
-      customConfig = res.body
-    }
+    customConfig = await server.config.getCustomConfig()
   })
 
   describe('Instance default NSFW policy', function () {
+
     it('Should display NSFW videos with display default NSFW policy', async function () {
-      const resConfig = await getConfig(server.url)
-      const serverConfig: ServerConfig = resConfig.body
+      const serverConfig = await server.config.getConfig()
       expect(serverConfig.instance.defaultNSFWPolicy).to.equal('display')
 
-      for (const res of await getVideosFunctions()) {
-        expect(res.body.total).to.equal(2)
+      for (const body of await getVideosFunctions()) {
+        expect(body.total).to.equal(2)
 
-        const videos = res.body.data
+        const videos = body.data
         expect(videos).to.have.lengthOf(2)
         expect(videos[0].name).to.equal('normal')
         expect(videos[1].name).to.equal('nsfw')
@@ -120,16 +99,15 @@ describe('Test video NSFW policy', function () {
 
     it('Should not display NSFW videos with do_not_list default NSFW policy', async function () {
       customConfig.instance.defaultNSFWPolicy = 'do_not_list'
-      await updateCustomConfig(server.url, server.accessToken, customConfig)
+      await server.config.updateCustomConfig({ newCustomConfig: customConfig })
 
-      const resConfig = await getConfig(server.url)
-      const serverConfig: ServerConfig = resConfig.body
+      const serverConfig = await server.config.getConfig()
       expect(serverConfig.instance.defaultNSFWPolicy).to.equal('do_not_list')
 
-      for (const res of await getVideosFunctions()) {
-        expect(res.body.total).to.equal(1)
+      for (const body of await getVideosFunctions()) {
+        expect(body.total).to.equal(1)
 
-        const videos = res.body.data
+        const videos = body.data
         expect(videos).to.have.lengthOf(1)
         expect(videos[0].name).to.equal('normal')
       }
@@ -137,16 +115,15 @@ describe('Test video NSFW policy', function () {
 
     it('Should display NSFW videos with blur default NSFW policy', async function () {
       customConfig.instance.defaultNSFWPolicy = 'blur'
-      await updateCustomConfig(server.url, server.accessToken, customConfig)
+      await server.config.updateCustomConfig({ newCustomConfig: customConfig })
 
-      const resConfig = await getConfig(server.url)
-      const serverConfig: ServerConfig = resConfig.body
+      const serverConfig = await server.config.getConfig()
       expect(serverConfig.instance.defaultNSFWPolicy).to.equal('blur')
 
-      for (const res of await getVideosFunctions()) {
-        expect(res.body.total).to.equal(2)
+      for (const body of await getVideosFunctions()) {
+        expect(body.total).to.equal(2)
 
-        const videos = res.body.data
+        const videos = body.data
         expect(videos).to.have.lengthOf(2)
         expect(videos[0].name).to.equal('normal')
         expect(videos[1].name).to.equal('nsfw')
@@ -159,24 +136,22 @@ describe('Test video NSFW policy', function () {
     it('Should create a user having the default nsfw policy', async function () {
       const username = 'user1'
       const password = 'my super password'
-      await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
-
-      userAccessToken = await userLogin(server, { username, password })
+      await server.users.create({ username: username, password: password })
 
-      const res = await getMyUserInformation(server.url, userAccessToken)
-      const user = res.body
+      userAccessToken = await server.login.getAccessToken({ username, password })
 
+      const user = await server.users.getMyInfo({ token: userAccessToken })
       expect(user.nsfwPolicy).to.equal('blur')
     })
 
     it('Should display NSFW videos with blur user NSFW policy', async function () {
       customConfig.instance.defaultNSFWPolicy = 'do_not_list'
-      await updateCustomConfig(server.url, server.accessToken, customConfig)
+      await server.config.updateCustomConfig({ newCustomConfig: customConfig })
 
-      for (const res of await getVideosFunctions(userAccessToken)) {
-        expect(res.body.total).to.equal(2)
+      for (const body of await getVideosFunctions(userAccessToken)) {
+        expect(body.total).to.equal(2)
 
-        const videos = res.body.data
+        const videos = body.data
         expect(videos).to.have.lengthOf(2)
         expect(videos[0].name).to.equal('normal')
         expect(videos[1].name).to.equal('nsfw')
@@ -184,16 +159,12 @@ describe('Test video NSFW policy', function () {
     })
 
     it('Should display NSFW videos with display user NSFW policy', async function () {
-      await updateMyUser({
-        url: server.url,
-        accessToken: server.accessToken,
-        nsfwPolicy: 'display'
-      })
+      await server.users.updateMe({ nsfwPolicy: 'display' })
 
-      for (const res of await getVideosFunctions(server.accessToken)) {
-        expect(res.body.total).to.equal(2)
+      for (const body of await getVideosFunctions(server.accessToken)) {
+        expect(body.total).to.equal(2)
 
-        const videos = res.body.data
+        const videos = body.data
         expect(videos).to.have.lengthOf(2)
         expect(videos[0].name).to.equal('normal')
         expect(videos[1].name).to.equal('nsfw')
@@ -201,56 +172,51 @@ describe('Test video NSFW policy', function () {
     })
 
     it('Should not display NSFW videos with do_not_list user NSFW policy', async function () {
-      await updateMyUser({
-        url: server.url,
-        accessToken: server.accessToken,
-        nsfwPolicy: 'do_not_list'
-      })
+      await server.users.updateMe({ nsfwPolicy: 'do_not_list' })
 
-      for (const res of await getVideosFunctions(server.accessToken)) {
-        expect(res.body.total).to.equal(1)
+      for (const body of await getVideosFunctions(server.accessToken)) {
+        expect(body.total).to.equal(1)
 
-        const videos = res.body.data
+        const videos = body.data
         expect(videos).to.have.lengthOf(1)
         expect(videos[0].name).to.equal('normal')
       }
     })
 
     it('Should be able to see my NSFW videos even with do_not_list user NSFW policy', async function () {
-      const res = await getMyVideos(server.url, server.accessToken, 0, 5)
-      expect(res.body.total).to.equal(2)
+      const { total, data } = await server.videos.listMyVideos()
+      expect(total).to.equal(2)
 
-      const videos = res.body.data
-      expect(videos).to.have.lengthOf(2)
-      expect(videos[0].name).to.equal('normal')
-      expect(videos[1].name).to.equal('nsfw')
+      expect(data).to.have.lengthOf(2)
+      expect(data[0].name).to.equal('normal')
+      expect(data[1].name).to.equal('nsfw')
     })
 
     it('Should display NSFW videos when the nsfw param === true', async function () {
-      for (const res of await getVideosFunctions(server.accessToken, { nsfw: true })) {
-        expect(res.body.total).to.equal(1)
+      for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'true' })) {
+        expect(body.total).to.equal(1)
 
-        const videos = res.body.data
+        const videos = body.data
         expect(videos).to.have.lengthOf(1)
         expect(videos[0].name).to.equal('nsfw')
       }
     })
 
     it('Should hide NSFW videos when the nsfw param === true', async function () {
-      for (const res of await getVideosFunctions(server.accessToken, { nsfw: false })) {
-        expect(res.body.total).to.equal(1)
+      for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'false' })) {
+        expect(body.total).to.equal(1)
 
-        const videos = res.body.data
+        const videos = body.data
         expect(videos).to.have.lengthOf(1)
         expect(videos[0].name).to.equal('normal')
       }
     })
 
     it('Should display both videos when the nsfw param === both', async function () {
-      for (const res of await getVideosFunctions(server.accessToken, { nsfw: 'both' })) {
-        expect(res.body.total).to.equal(2)
+      for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'both' })) {
+        expect(body.total).to.equal(2)
 
-        const videos = res.body.data
+        const videos = body.data
         expect(videos).to.have.lengthOf(2)
         expect(videos[0].name).to.equal('normal')
         expect(videos[1].name).to.equal('nsfw')
index a93a0b7de816bc19276a31f6ff5941922b09858e..f0b2ca169fc19f00f074cb5f25cd645275a2d517 100644 (file)
@@ -1,21 +1,15 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import {
-  addVideoInPlaylist,
   cleanupTests,
-  createVideoPlaylist,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getVideoPlaylistsList,
-  removeVideoFromPlaylist,
-  reorderVideosPlaylist,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
   setDefaultVideoChannel,
   testImage,
-  uploadVideoAndGetId,
   waitJobs
 } from '../../../../shared/extra-utils'
 import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
@@ -23,10 +17,10 @@ import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/
 const expect = chai.expect
 
 describe('Playlist thumbnail', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
 
-  let playlistWithoutThumbnail: number
-  let playlistWithThumbnail: number
+  let playlistWithoutThumbnailId: number
+  let playlistWithThumbnailId: number
 
   let withThumbnailE1: number
   let withThumbnailE2: number
@@ -36,22 +30,22 @@ describe('Playlist thumbnail', function () {
   let video1: number
   let video2: number
 
-  async function getPlaylistWithoutThumbnail (server: ServerInfo) {
-    const res = await getVideoPlaylistsList(server.url, 0, 10)
+  async function getPlaylistWithoutThumbnail (server: PeerTubeServer) {
+    const body = await server.playlists.list({ start: 0, count: 10 })
 
-    return res.body.data.find(p => p.displayName === 'playlist without thumbnail')
+    return body.data.find(p => p.displayName === 'playlist without thumbnail')
   }
 
-  async function getPlaylistWithThumbnail (server: ServerInfo) {
-    const res = await getVideoPlaylistsList(server.url, 0, 10)
+  async function getPlaylistWithThumbnail (server: PeerTubeServer) {
+    const body = await server.playlists.list({ start: 0, count: 10 })
 
-    return res.body.data.find(p => p.displayName === 'playlist with thumbnail')
+    return body.data.find(p => p.displayName === 'playlist with thumbnail')
   }
 
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: false } })
+    servers = await createMultipleServers(2, { transcoding: { enabled: false } })
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -60,8 +54,8 @@ describe('Playlist thumbnail', function () {
     // Server 1 and server 2 follow each other
     await doubleFollow(servers[0], servers[1])
 
-    video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).id
-    video2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).id
+    video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).id
+    video2 = (await servers[0].videos.quickUpload({ name: 'video 2' })).id
 
     await waitJobs(servers)
   })
@@ -69,24 +63,20 @@ describe('Playlist thumbnail', function () {
   it('Should automatically update the thumbnail when adding an element', async function () {
     this.timeout(30000)
 
-    const res = await createVideoPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistAttrs: {
+    const created = await servers[1].playlists.create({
+      attributes: {
         displayName: 'playlist without thumbnail',
         privacy: VideoPlaylistPrivacy.PUBLIC,
-        videoChannelId: servers[1].videoChannel.id
+        videoChannelId: servers[1].store.channel.id
       }
     })
-    playlistWithoutThumbnail = res.body.videoPlaylist.id
+    playlistWithoutThumbnailId = created.id
 
-    const res2 = await addVideoInPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistWithoutThumbnail,
-      elementAttrs: { videoId: video1 }
+    const added = await servers[1].playlists.addElement({
+      playlistId: playlistWithoutThumbnailId,
+      attributes: { videoId: video1 }
     })
-    withoutThumbnailE1 = res2.body.videoPlaylistElement.id
+    withoutThumbnailE1 = added.id
 
     await waitJobs(servers)
 
@@ -99,25 +89,21 @@ describe('Playlist thumbnail', function () {
   it('Should not update the thumbnail if we explicitly uploaded a thumbnail', async function () {
     this.timeout(30000)
 
-    const res = await createVideoPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistAttrs: {
+    const created = await servers[1].playlists.create({
+      attributes: {
         displayName: 'playlist with thumbnail',
         privacy: VideoPlaylistPrivacy.PUBLIC,
-        videoChannelId: servers[1].videoChannel.id,
+        videoChannelId: servers[1].store.channel.id,
         thumbnailfile: 'thumbnail.jpg'
       }
     })
-    playlistWithThumbnail = res.body.videoPlaylist.id
+    playlistWithThumbnailId = created.id
 
-    const res2 = await addVideoInPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistWithThumbnail,
-      elementAttrs: { videoId: video1 }
+    const added = await servers[1].playlists.addElement({
+      playlistId: playlistWithThumbnailId,
+      attributes: { videoId: video1 }
     })
-    withThumbnailE1 = res2.body.videoPlaylistElement.id
+    withThumbnailE1 = added.id
 
     await waitJobs(servers)
 
@@ -130,19 +116,15 @@ describe('Playlist thumbnail', function () {
   it('Should automatically update the thumbnail when moving the first element', async function () {
     this.timeout(30000)
 
-    const res = await addVideoInPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistWithoutThumbnail,
-      elementAttrs: { videoId: video2 }
+    const added = await servers[1].playlists.addElement({
+      playlistId: playlistWithoutThumbnailId,
+      attributes: { videoId: video2 }
     })
-    withoutThumbnailE2 = res.body.videoPlaylistElement.id
+    withoutThumbnailE2 = added.id
 
-    await reorderVideosPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistWithoutThumbnail,
-      elementAttrs: {
+    await servers[1].playlists.reorderElements({
+      playlistId: playlistWithoutThumbnailId,
+      attributes: {
         startPosition: 1,
         insertAfterPosition: 2
       }
@@ -159,19 +141,15 @@ describe('Playlist thumbnail', function () {
   it('Should not update the thumbnail when moving the first element if we explicitly uploaded a thumbnail', async function () {
     this.timeout(30000)
 
-    const res = await addVideoInPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistWithThumbnail,
-      elementAttrs: { videoId: video2 }
+    const added = await servers[1].playlists.addElement({
+      playlistId: playlistWithThumbnailId,
+      attributes: { videoId: video2 }
     })
-    withThumbnailE2 = res.body.videoPlaylistElement.id
+    withThumbnailE2 = added.id
 
-    await reorderVideosPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistWithThumbnail,
-      elementAttrs: {
+    await servers[1].playlists.reorderElements({
+      playlistId: playlistWithThumbnailId,
+      attributes: {
         startPosition: 1,
         insertAfterPosition: 2
       }
@@ -188,11 +166,9 @@ describe('Playlist thumbnail', function () {
   it('Should automatically update the thumbnail when deleting the first element', async function () {
     this.timeout(30000)
 
-    await removeVideoFromPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistWithoutThumbnail,
-      playlistElementId: withoutThumbnailE1
+    await servers[1].playlists.removeElement({
+      playlistId: playlistWithoutThumbnailId,
+      elementId: withoutThumbnailE1
     })
 
     await waitJobs(servers)
@@ -206,11 +182,9 @@ describe('Playlist thumbnail', function () {
   it('Should not update the thumbnail when deleting the first element if we explicitly uploaded a thumbnail', async function () {
     this.timeout(30000)
 
-    await removeVideoFromPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistWithThumbnail,
-      playlistElementId: withThumbnailE1
+    await servers[1].playlists.removeElement({
+      playlistId: playlistWithThumbnailId,
+      elementId: withThumbnailE1
     })
 
     await waitJobs(servers)
@@ -224,11 +198,9 @@ describe('Playlist thumbnail', function () {
   it('Should the thumbnail when we delete the last element', async function () {
     this.timeout(30000)
 
-    await removeVideoFromPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistWithoutThumbnail,
-      playlistElementId: withoutThumbnailE2
+    await servers[1].playlists.removeElement({
+      playlistId: playlistWithoutThumbnailId,
+      elementId: withoutThumbnailE2
     })
 
     await waitJobs(servers)
@@ -242,11 +214,9 @@ describe('Playlist thumbnail', function () {
   it('Should not update the thumbnail when we delete the last element if we explicitly uploaded a thumbnail', async function () {
     this.timeout(30000)
 
-    await removeVideoFromPlaylist({
-      url: servers[1].url,
-      token: servers[1].accessToken,
-      playlistId: playlistWithThumbnail,
-      playlistElementId: withThumbnailE2
+    await servers[1].playlists.removeElement({
+      playlistId: playlistWithThumbnailId,
+      elementId: withThumbnailE2
     })
 
     await waitJobs(servers)
index da8de054b612dae26e971d3174731ef8e894dba2..f42aee2ffb05a5d7d6f0d77bb2306902a5c8194c 100644 (file)
@@ -2,71 +2,33 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
-  addVideoChannel,
-  addVideoInPlaylist,
-  addVideoToBlacklist,
   checkPlaylistFilesWereRemoved,
   cleanupTests,
-  createUser,
-  createVideoPlaylist,
-  deleteVideoChannel,
-  deleteVideoPlaylist,
+  createMultipleServers,
   doubleFollow,
-  doVideosExistInMyPlaylist,
-  flushAndRunMultipleServers,
-  generateUserAccessToken,
-  getAccessToken,
-  getAccountPlaylistsList,
-  getAccountPlaylistsListWithToken,
-  getMyUserInformation,
-  getPlaylistVideos,
-  getVideoChannelPlaylistsList,
-  getVideoPlaylist,
-  getVideoPlaylistPrivacies,
-  getVideoPlaylistsList,
-  getVideoPlaylistWithToken,
-  removeUser,
-  removeVideoFromBlacklist,
-  removeVideoFromPlaylist,
-  reorderVideosPlaylist,
-  ServerInfo,
+  PeerTubeServer,
+  PlaylistsCommand,
   setAccessTokensToServers,
   setDefaultVideoChannel,
   testImage,
-  unfollow,
-  updateVideo,
-  updateVideoPlaylist,
-  updateVideoPlaylistElement,
-  uploadVideo,
-  uploadVideoAndGetId,
-  userLogin,
   wait,
   waitJobs
-} from '../../../../shared/extra-utils'
+} from '@shared/extra-utils'
 import {
-  addAccountToAccountBlocklist,
-  addAccountToServerBlocklist,
-  addServerToAccountBlocklist,
-  addServerToServerBlocklist,
-  removeAccountFromAccountBlocklist,
-  removeAccountFromServerBlocklist,
-  removeServerFromAccountBlocklist,
-  removeServerFromServerBlocklist
-} from '../../../../shared/extra-utils/users/blocklist'
-import { User } from '../../../../shared/models/users'
-import { VideoPlaylistCreateResult, VideoPrivacy } from '../../../../shared/models/videos'
-import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model'
-import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../../shared/models/videos/playlist/video-playlist-element.model'
-import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
-import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
-import { VideoPlaylist } from '../../../../shared/models/videos/playlist/video-playlist.model'
+  HttpStatusCode,
+  VideoPlaylist,
+  VideoPlaylistCreateResult,
+  VideoPlaylistElementType,
+  VideoPlaylistPrivacy,
+  VideoPlaylistType,
+  VideoPrivacy
+} from '@shared/models'
 
 const expect = chai.expect
 
 async function checkPlaylistElementType (
-  servers: ServerInfo[],
+  servers: PeerTubeServer[],
   playlistId: string,
   type: VideoPlaylistElementType,
   position: number,
@@ -74,10 +36,10 @@ async function checkPlaylistElementType (
   total: number
 ) {
   for (const server of servers) {
-    const res = await getPlaylistVideos(server.url, server.accessToken, playlistId, 0, 10)
-    expect(res.body.total).to.equal(total)
+    const body = await server.playlists.listVideos({ token: server.accessToken, playlistId, start: 0, count: 10 })
+    expect(body.total).to.equal(total)
 
-    const videoElement: VideoPlaylistElement = res.body.data.find((e: VideoPlaylistElement) => e.position === position)
+    const videoElement = body.data.find(e => e.position === position)
     expect(videoElement.type).to.equal(type, 'On server ' + server.url)
 
     if (type === VideoPlaylistElementType.REGULAR) {
@@ -90,11 +52,11 @@ async function checkPlaylistElementType (
 }
 
 describe('Test video playlists', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
 
   let playlistServer2Id1: number
   let playlistServer2Id2: number
-  let playlistServer2UUID2: number
+  let playlistServer2UUID2: string
 
   let playlistServer1Id: number
   let playlistServer1UUID: string
@@ -106,12 +68,14 @@ describe('Test video playlists', function () {
 
   let nsfwVideoServer1: number
 
-  let userAccessTokenServer1: string
+  let userTokenServer1: string
+
+  let commands: PlaylistsCommand[]
 
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(3, { transcoding: { enabled: false } })
+    servers = await createMultipleServers(3, { transcoding: { enabled: false } })
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -122,86 +86,78 @@ describe('Test video playlists', function () {
     // Server 1 and server 3 follow each other
     await doubleFollow(servers[0], servers[2])
 
+    commands = servers.map(s => s.playlists)
+
     {
-      servers[0].videos = []
-      servers[1].videos = []
-      servers[2].videos = []
+      servers[0].store.videos = []
+      servers[1].store.videos = []
+      servers[2].store.videos = []
 
       for (const server of servers) {
         for (let i = 0; i < 7; i++) {
           const name = `video ${i} server ${server.serverNumber}`
-          const resVideo = await uploadVideo(server.url, server.accessToken, { name, nsfw: false })
+          const video = await server.videos.upload({ attributes: { name, nsfw: false } })
 
-          server.videos.push(resVideo.body.video)
+          server.store.videos.push(video)
         }
       }
     }
 
-    nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'NSFW video', nsfw: true })).id
+    nsfwVideoServer1 = (await servers[0].videos.quickUpload({ name: 'NSFW video', nsfw: true })).id
 
-    {
-      await createUser({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
-        username: 'user1',
-        password: 'password'
-      })
-      userAccessTokenServer1 = await getAccessToken(servers[0].url, 'user1', 'password')
-    }
+    userTokenServer1 = await servers[0].users.generateUserAndToken('user1')
 
     await waitJobs(servers)
   })
 
   describe('Get default playlists', function () {
+
     it('Should list video playlist privacies', async function () {
-      const res = await getVideoPlaylistPrivacies(servers[0].url)
+      const privacies = await commands[0].getPrivacies()
 
-      const privacies = res.body
       expect(Object.keys(privacies)).to.have.length.at.least(3)
-
       expect(privacies[3]).to.equal('Private')
     })
 
     it('Should list watch later playlist', async function () {
-      const url = servers[0].url
-      const accessToken = servers[0].accessToken
+      const token = servers[0].accessToken
 
       {
-        const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER)
+        const body = await commands[0].listByAccount({ token, handle: 'root', playlistType: VideoPlaylistType.WATCH_LATER })
 
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        expect(body.total).to.equal(1)
+        expect(body.data).to.have.lengthOf(1)
 
-        const playlist: VideoPlaylist = res.body.data[0]
+        const playlist = body.data[0]
         expect(playlist.displayName).to.equal('Watch later')
         expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER)
         expect(playlist.type.label).to.equal('Watch later')
       }
 
       {
-        const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.REGULAR)
+        const body = await commands[0].listByAccount({ token, handle: 'root', playlistType: VideoPlaylistType.REGULAR })
 
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
+        expect(body.total).to.equal(0)
+        expect(body.data).to.have.lengthOf(0)
       }
 
       {
-        const res = await getAccountPlaylistsList(url, 'root', 0, 5)
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
+        const body = await commands[0].listByAccount({ handle: 'root' })
+        expect(body.total).to.equal(0)
+        expect(body.data).to.have.lengthOf(0)
       }
     })
 
     it('Should get private playlist for a classic user', async function () {
-      const token = await generateUserAccessToken(servers[0], 'toto')
+      const token = await servers[0].users.generateUserAndToken('toto')
 
-      const res = await getAccountPlaylistsListWithToken(servers[0].url, token, 'toto', 0, 5)
+      const body = await commands[0].listByAccount({ token, handle: 'toto' })
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
+      expect(body.total).to.equal(1)
+      expect(body.data).to.have.lengthOf(1)
 
-      const playlistId = res.body.data[0].id
-      await getPlaylistVideos(servers[0].url, token, playlistId, 0, 5)
+      const playlistId = body.data[0].id
+      await commands[0].listVideos({ token, playlistId })
     })
   })
 
@@ -210,15 +166,13 @@ describe('Test video playlists', function () {
     it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () {
       this.timeout(30000)
 
-      await createVideoPlaylist({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        playlistAttrs: {
+      await commands[0].create({
+        attributes: {
           displayName: 'my super playlist',
           privacy: VideoPlaylistPrivacy.PUBLIC,
           description: 'my super description',
           thumbnailfile: 'thumbnail.jpg',
-          videoChannelId: servers[0].videoChannel.id
+          videoChannelId: servers[0].store.channel.id
         }
       })
 
@@ -227,14 +181,13 @@ describe('Test video playlists', function () {
       await wait(3000)
 
       for (const server of servers) {
-        const res = await getVideoPlaylistsList(server.url, 0, 5)
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data).to.have.lengthOf(1)
+        const body = await server.playlists.list({ start: 0, count: 5 })
+        expect(body.total).to.equal(1)
+        expect(body.data).to.have.lengthOf(1)
 
-        const playlistFromList = res.body.data[0] as VideoPlaylist
+        const playlistFromList = body.data[0]
 
-        const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid)
-        const playlistFromGet = res2.body as VideoPlaylist
+        const playlistFromGet = await server.playlists.get({ playlistId: playlistFromList.uuid })
 
         for (const playlist of [ playlistFromGet, playlistFromList ]) {
           expect(playlist.id).to.be.a('number')
@@ -264,46 +217,38 @@ describe('Test video playlists', function () {
       this.timeout(30000)
 
       {
-        const res = await createVideoPlaylist({
-          url: servers[1].url,
-          token: servers[1].accessToken,
-          playlistAttrs: {
+        const playlist = await servers[1].playlists.create({
+          attributes: {
             displayName: 'playlist 2',
             privacy: VideoPlaylistPrivacy.PUBLIC,
-            videoChannelId: servers[1].videoChannel.id
+            videoChannelId: servers[1].store.channel.id
           }
         })
-        playlistServer2Id1 = res.body.videoPlaylist.id
+        playlistServer2Id1 = playlist.id
       }
 
       {
-        const res = await createVideoPlaylist({
-          url: servers[1].url,
-          token: servers[1].accessToken,
-          playlistAttrs: {
+        const playlist = await servers[1].playlists.create({
+          attributes: {
             displayName: 'playlist 3',
             privacy: VideoPlaylistPrivacy.PUBLIC,
             thumbnailfile: 'thumbnail.jpg',
-            videoChannelId: servers[1].videoChannel.id
+            videoChannelId: servers[1].store.channel.id
           }
         })
 
-        playlistServer2Id2 = res.body.videoPlaylist.id
-        playlistServer2UUID2 = res.body.videoPlaylist.uuid
+        playlistServer2Id2 = playlist.id
+        playlistServer2UUID2 = playlist.uuid
       }
 
       for (const id of [ playlistServer2Id1, playlistServer2Id2 ]) {
-        await addVideoInPlaylist({
-          url: servers[1].url,
-          token: servers[1].accessToken,
+        await servers[1].playlists.addElement({
           playlistId: id,
-          elementAttrs: { videoId: servers[1].videos[0].id, startTimestamp: 1, stopTimestamp: 2 }
+          attributes: { videoId: servers[1].store.videos[0].id, startTimestamp: 1, stopTimestamp: 2 }
         })
-        await addVideoInPlaylist({
-          url: servers[1].url,
-          token: servers[1].accessToken,
+        await servers[1].playlists.addElement({
           playlistId: id,
-          elementAttrs: { videoId: servers[1].videos[1].id }
+          attributes: { videoId: servers[1].store.videos[1].id }
         })
       }
 
@@ -311,20 +256,20 @@ describe('Test video playlists', function () {
       await wait(3000)
 
       for (const server of [ servers[0], servers[1] ]) {
-        const res = await getVideoPlaylistsList(server.url, 0, 5)
+        const body = await server.playlists.list({ start: 0, count: 5 })
 
-        const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
+        const playlist2 = body.data.find(p => p.displayName === 'playlist 2')
         expect(playlist2).to.not.be.undefined
         await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath)
 
-        const playlist3 = res.body.data.find(p => p.displayName === 'playlist 3')
+        const playlist3 = body.data.find(p => p.displayName === 'playlist 3')
         expect(playlist3).to.not.be.undefined
         await testImage(server.url, 'thumbnail', playlist3.thumbnailPath)
       }
 
-      const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
-      expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined
-      expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined
+      const body = await servers[2].playlists.list({ start: 0, count: 5 })
+      expect(body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined
+      expect(body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined
     })
 
     it('Should have the playlist on server 3 after a new follow', async function () {
@@ -333,13 +278,13 @@ describe('Test video playlists', function () {
       // Server 2 and server 3 follow each other
       await doubleFollow(servers[1], servers[2])
 
-      const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
+      const body = await servers[2].playlists.list({ start: 0, count: 5 })
 
-      const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
+      const playlist2 = body.data.find(p => p.displayName === 'playlist 2')
       expect(playlist2).to.not.be.undefined
       await testImage(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath)
 
-      expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined
+      expect(body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined
     })
   })
 
@@ -349,22 +294,20 @@ describe('Test video playlists', function () {
       this.timeout(30000)
 
       {
-        const res = await getVideoPlaylistsList(servers[2].url, 1, 2, 'createdAt')
-
-        expect(res.body.total).to.equal(3)
+        const body = await servers[2].playlists.list({ start: 1, count: 2, sort: 'createdAt' })
+        expect(body.total).to.equal(3)
 
-        const data: VideoPlaylist[] = res.body.data
+        const data = body.data
         expect(data).to.have.lengthOf(2)
         expect(data[0].displayName).to.equal('playlist 2')
         expect(data[1].displayName).to.equal('playlist 3')
       }
 
       {
-        const res = await getVideoPlaylistsList(servers[2].url, 1, 2, '-createdAt')
+        const body = await servers[2].playlists.list({ start: 1, count: 2, sort: '-createdAt' })
+        expect(body.total).to.equal(3)
 
-        expect(res.body.total).to.equal(3)
-
-        const data: VideoPlaylist[] = res.body.data
+        const data = body.data
         expect(data).to.have.lengthOf(2)
         expect(data[0].displayName).to.equal('playlist 2')
         expect(data[1].displayName).to.equal('my super playlist')
@@ -375,11 +318,10 @@ describe('Test video playlists', function () {
       this.timeout(30000)
 
       {
-        const res = await getVideoChannelPlaylistsList(servers[0].url, 'root_channel', 0, 2, '-createdAt')
-
-        expect(res.body.total).to.equal(1)
+        const body = await commands[0].listByChannel({ handle: 'root_channel', start: 0, count: 2, sort: '-createdAt' })
+        expect(body.total).to.equal(1)
 
-        const data: VideoPlaylist[] = res.body.data
+        const data = body.data
         expect(data).to.have.lengthOf(1)
         expect(data[0].displayName).to.equal('my super playlist')
       }
@@ -389,41 +331,37 @@ describe('Test video playlists', function () {
       this.timeout(30000)
 
       {
-        const res = await getAccountPlaylistsList(servers[1].url, 'root', 1, 2, '-createdAt')
+        const body = await servers[1].playlists.listByAccount({ handle: 'root', start: 1, count: 2, sort: '-createdAt' })
+        expect(body.total).to.equal(2)
 
-        expect(res.body.total).to.equal(2)
-
-        const data: VideoPlaylist[] = res.body.data
+        const data = body.data
         expect(data).to.have.lengthOf(1)
         expect(data[0].displayName).to.equal('playlist 2')
       }
 
       {
-        const res = await getAccountPlaylistsList(servers[1].url, 'root', 1, 2, 'createdAt')
-
-        expect(res.body.total).to.equal(2)
+        const body = await servers[1].playlists.listByAccount({ handle: 'root', start: 1, count: 2, sort: 'createdAt' })
+        expect(body.total).to.equal(2)
 
-        const data: VideoPlaylist[] = res.body.data
+        const data = body.data
         expect(data).to.have.lengthOf(1)
         expect(data[0].displayName).to.equal('playlist 3')
       }
 
       {
-        const res = await getAccountPlaylistsList(servers[1].url, 'root', 0, 10, 'createdAt', '3')
+        const body = await servers[1].playlists.listByAccount({ handle: 'root', sort: 'createdAt', search: '3' })
+        expect(body.total).to.equal(1)
 
-        expect(res.body.total).to.equal(1)
-
-        const data: VideoPlaylist[] = res.body.data
+        const data = body.data
         expect(data).to.have.lengthOf(1)
         expect(data[0].displayName).to.equal('playlist 3')
       }
 
       {
-        const res = await getAccountPlaylistsList(servers[1].url, 'root', 0, 10, 'createdAt', '4')
-
-        expect(res.body.total).to.equal(0)
+        const body = await servers[1].playlists.listByAccount({ handle: 'root', sort: 'createdAt', search: '4' })
+        expect(body.total).to.equal(0)
 
-        const data: VideoPlaylist[] = res.body.data
+        const data = body.data
         expect(data).to.have.lengthOf(0)
       }
     })
@@ -437,28 +375,22 @@ describe('Test video playlists', function () {
       this.timeout(30000)
 
       {
-        const res = await createVideoPlaylist({
-          url: servers[1].url,
-          token: servers[1].accessToken,
-          playlistAttrs: {
+        unlistedPlaylist = await servers[1].playlists.create({
+          attributes: {
             displayName: 'playlist unlisted',
             privacy: VideoPlaylistPrivacy.UNLISTED,
-            videoChannelId: servers[1].videoChannel.id
+            videoChannelId: servers[1].store.channel.id
           }
         })
-        unlistedPlaylist = res.body.videoPlaylist
       }
 
       {
-        const res = await createVideoPlaylist({
-          url: servers[1].url,
-          token: servers[1].accessToken,
-          playlistAttrs: {
+        privatePlaylist = await servers[1].playlists.create({
+          attributes: {
             displayName: 'playlist private',
             privacy: VideoPlaylistPrivacy.PRIVATE
           }
         })
-        privatePlaylist = res.body.videoPlaylist
       }
 
       await waitJobs(servers)
@@ -468,15 +400,15 @@ describe('Test video playlists', function () {
     it('Should not list unlisted or private playlists', async function () {
       for (const server of servers) {
         const results = [
-          await getAccountPlaylistsList(server.url, 'root@localhost:' + servers[1].port, 0, 5, '-createdAt'),
-          await getVideoPlaylistsList(server.url, 0, 2, '-createdAt')
+          await server.playlists.listByAccount({ handle: 'root@localhost:' + servers[1].port, sort: '-createdAt' }),
+          await server.playlists.list({ start: 0, count: 2, sort: '-createdAt' })
         ]
 
-        expect(results[0].body.total).to.equal(2)
-        expect(results[1].body.total).to.equal(3)
+        expect(results[0].total).to.equal(2)
+        expect(results[1].total).to.equal(3)
 
-        for (const res of results) {
-          const data: VideoPlaylist[] = res.body.data
+        for (const body of results) {
+          const data = body.data
           expect(data).to.have.lengthOf(2)
           expect(data[0].displayName).to.equal('playlist 3')
           expect(data[1].displayName).to.equal('playlist 2')
@@ -485,23 +417,23 @@ describe('Test video playlists', function () {
     })
 
     it('Should not get unlisted playlist using only the id', async function () {
-      await getVideoPlaylist(servers[1].url, unlistedPlaylist.id, 404)
+      await servers[1].playlists.get({ playlistId: unlistedPlaylist.id, expectedStatus: 404 })
     })
 
     it('Should get unlisted plyaylist using uuid or shortUUID', async function () {
-      await getVideoPlaylist(servers[1].url, unlistedPlaylist.uuid)
-      await getVideoPlaylist(servers[1].url, unlistedPlaylist.shortUUID)
+      await servers[1].playlists.get({ playlistId: unlistedPlaylist.uuid })
+      await servers[1].playlists.get({ playlistId: unlistedPlaylist.shortUUID })
     })
 
     it('Should not get private playlist without token', async function () {
       for (const id of [ privatePlaylist.id, privatePlaylist.uuid, privatePlaylist.shortUUID ]) {
-        await getVideoPlaylist(servers[1].url, id, 401)
+        await servers[1].playlists.get({ playlistId: id, expectedStatus: 401 })
       }
     })
 
     it('Should get private playlist with a token', async function () {
       for (const id of [ privatePlaylist.id, privatePlaylist.uuid, privatePlaylist.shortUUID ]) {
-        await getVideoPlaylistWithToken(servers[1].url, servers[1].accessToken, id)
+        await servers[1].playlists.get({ token: servers[1].accessToken, playlistId: id })
       }
     })
   })
@@ -511,15 +443,13 @@ describe('Test video playlists', function () {
     it('Should update a playlist', async function () {
       this.timeout(30000)
 
-      await updateVideoPlaylist({
-        url: servers[1].url,
-        token: servers[1].accessToken,
-        playlistAttrs: {
+      await servers[1].playlists.update({
+        attributes: {
           displayName: 'playlist 3 updated',
           description: 'description updated',
           privacy: VideoPlaylistPrivacy.UNLISTED,
           thumbnailfile: 'thumbnail.jpg',
-          videoChannelId: servers[1].videoChannel.id
+          videoChannelId: servers[1].store.channel.id
         },
         playlistId: playlistServer2Id2
       })
@@ -527,8 +457,7 @@ describe('Test video playlists', function () {
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideoPlaylist(server.url, playlistServer2UUID2)
-        const playlist: VideoPlaylist = res.body
+        const playlist = await server.playlists.get({ playlistId: playlistServer2UUID2 })
 
         expect(playlist.displayName).to.equal('playlist 3 updated')
         expect(playlist.description).to.equal('description updated')
@@ -554,39 +483,37 @@ describe('Test video playlists', function () {
     it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () {
       this.timeout(30000)
 
-      const addVideo = (elementAttrs: any) => {
-        return addVideoInPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: playlistServer1Id, elementAttrs })
+      const addVideo = (attributes: any) => {
+        return commands[0].addElement({ playlistId: playlistServer1Id, attributes })
       }
 
-      const res = await createVideoPlaylist({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        playlistAttrs: {
+      const playlist = await commands[0].create({
+        attributes: {
           displayName: 'playlist 4',
           privacy: VideoPlaylistPrivacy.PUBLIC,
-          videoChannelId: servers[0].videoChannel.id
+          videoChannelId: servers[0].store.channel.id
         }
       })
 
-      playlistServer1Id = res.body.videoPlaylist.id
-      playlistServer1UUID = res.body.videoPlaylist.uuid
+      playlistServer1Id = playlist.id
+      playlistServer1UUID = playlist.uuid
 
-      await addVideo({ videoId: servers[0].videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 })
-      await addVideo({ videoId: servers[2].videos[1].uuid, startTimestamp: 35 })
-      await addVideo({ videoId: servers[2].videos[2].uuid })
+      await addVideo({ videoId: servers[0].store.videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 })
+      await addVideo({ videoId: servers[2].store.videos[1].uuid, startTimestamp: 35 })
+      await addVideo({ videoId: servers[2].store.videos[2].uuid })
       {
-        const res = await addVideo({ videoId: servers[0].videos[3].uuid, stopTimestamp: 35 })
-        playlistElementServer1Video4 = res.body.videoPlaylistElement.id
+        const element = await addVideo({ videoId: servers[0].store.videos[3].uuid, stopTimestamp: 35 })
+        playlistElementServer1Video4 = element.id
       }
 
       {
-        const res = await addVideo({ videoId: servers[0].videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 })
-        playlistElementServer1Video5 = res.body.videoPlaylistElement.id
+        const element = await addVideo({ videoId: servers[0].store.videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 })
+        playlistElementServer1Video5 = element.id
       }
 
       {
-        const res = await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 })
-        playlistElementNSFW = res.body.videoPlaylistElement.id
+        const element = await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 })
+        playlistElementNSFW = element.id
 
         await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 4 })
         await addVideo({ videoId: nsfwVideoServer1 })
@@ -599,64 +526,68 @@ describe('Test video playlists', function () {
       this.timeout(30000)
 
       for (const server of servers) {
-        const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
-
-        expect(res.body.total).to.equal(8)
-
-        const videoElements: VideoPlaylistElement[] = res.body.data
-        expect(videoElements).to.have.lengthOf(8)
-
-        expect(videoElements[0].video.name).to.equal('video 0 server 1')
-        expect(videoElements[0].position).to.equal(1)
-        expect(videoElements[0].startTimestamp).to.equal(15)
-        expect(videoElements[0].stopTimestamp).to.equal(28)
-
-        expect(videoElements[1].video.name).to.equal('video 1 server 3')
-        expect(videoElements[1].position).to.equal(2)
-        expect(videoElements[1].startTimestamp).to.equal(35)
-        expect(videoElements[1].stopTimestamp).to.be.null
-
-        expect(videoElements[2].video.name).to.equal('video 2 server 3')
-        expect(videoElements[2].position).to.equal(3)
-        expect(videoElements[2].startTimestamp).to.be.null
-        expect(videoElements[2].stopTimestamp).to.be.null
-
-        expect(videoElements[3].video.name).to.equal('video 3 server 1')
-        expect(videoElements[3].position).to.equal(4)
-        expect(videoElements[3].startTimestamp).to.be.null
-        expect(videoElements[3].stopTimestamp).to.equal(35)
-
-        expect(videoElements[4].video.name).to.equal('video 4 server 1')
-        expect(videoElements[4].position).to.equal(5)
-        expect(videoElements[4].startTimestamp).to.equal(45)
-        expect(videoElements[4].stopTimestamp).to.equal(60)
-
-        expect(videoElements[5].video.name).to.equal('NSFW video')
-        expect(videoElements[5].position).to.equal(6)
-        expect(videoElements[5].startTimestamp).to.equal(5)
-        expect(videoElements[5].stopTimestamp).to.be.null
-
-        expect(videoElements[6].video.name).to.equal('NSFW video')
-        expect(videoElements[6].position).to.equal(7)
-        expect(videoElements[6].startTimestamp).to.equal(4)
-        expect(videoElements[6].stopTimestamp).to.be.null
-
-        expect(videoElements[7].video.name).to.equal('NSFW video')
-        expect(videoElements[7].position).to.equal(8)
-        expect(videoElements[7].startTimestamp).to.be.null
-        expect(videoElements[7].stopTimestamp).to.be.null
-
-        const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2)
-        expect(res3.body.data).to.have.lengthOf(2)
+        {
+          const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
+
+          expect(body.total).to.equal(8)
+
+          const videoElements = body.data
+          expect(videoElements).to.have.lengthOf(8)
+
+          expect(videoElements[0].video.name).to.equal('video 0 server 1')
+          expect(videoElements[0].position).to.equal(1)
+          expect(videoElements[0].startTimestamp).to.equal(15)
+          expect(videoElements[0].stopTimestamp).to.equal(28)
+
+          expect(videoElements[1].video.name).to.equal('video 1 server 3')
+          expect(videoElements[1].position).to.equal(2)
+          expect(videoElements[1].startTimestamp).to.equal(35)
+          expect(videoElements[1].stopTimestamp).to.be.null
+
+          expect(videoElements[2].video.name).to.equal('video 2 server 3')
+          expect(videoElements[2].position).to.equal(3)
+          expect(videoElements[2].startTimestamp).to.be.null
+          expect(videoElements[2].stopTimestamp).to.be.null
+
+          expect(videoElements[3].video.name).to.equal('video 3 server 1')
+          expect(videoElements[3].position).to.equal(4)
+          expect(videoElements[3].startTimestamp).to.be.null
+          expect(videoElements[3].stopTimestamp).to.equal(35)
+
+          expect(videoElements[4].video.name).to.equal('video 4 server 1')
+          expect(videoElements[4].position).to.equal(5)
+          expect(videoElements[4].startTimestamp).to.equal(45)
+          expect(videoElements[4].stopTimestamp).to.equal(60)
+
+          expect(videoElements[5].video.name).to.equal('NSFW video')
+          expect(videoElements[5].position).to.equal(6)
+          expect(videoElements[5].startTimestamp).to.equal(5)
+          expect(videoElements[5].stopTimestamp).to.be.null
+
+          expect(videoElements[6].video.name).to.equal('NSFW video')
+          expect(videoElements[6].position).to.equal(7)
+          expect(videoElements[6].startTimestamp).to.equal(4)
+          expect(videoElements[6].stopTimestamp).to.be.null
+
+          expect(videoElements[7].video.name).to.equal('NSFW video')
+          expect(videoElements[7].position).to.equal(8)
+          expect(videoElements[7].startTimestamp).to.be.null
+          expect(videoElements[7].stopTimestamp).to.be.null
+        }
+
+        {
+          const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 2 })
+          expect(body.data).to.have.lengthOf(2)
+        }
       }
     })
   })
 
   describe('Element type', function () {
-    let groupUser1: ServerInfo[]
-    let groupWithoutToken1: ServerInfo[]
-    let group1: ServerInfo[]
-    let group2: ServerInfo[]
+    let groupUser1: PeerTubeServer[]
+    let groupWithoutToken1: PeerTubeServer[]
+    let group1: PeerTubeServer[]
+    let group2: PeerTubeServer[]
 
     let video1: string
     let video2: string
@@ -665,31 +596,30 @@ describe('Test video playlists', function () {
     before(async function () {
       this.timeout(60000)
 
-      groupUser1 = [ Object.assign({}, servers[0], { accessToken: userAccessTokenServer1 }) ]
+      groupUser1 = [ Object.assign({}, servers[0], { accessToken: userTokenServer1 }) ]
       groupWithoutToken1 = [ Object.assign({}, servers[0], { accessToken: undefined }) ]
       group1 = [ servers[0] ]
       group2 = [ servers[1], servers[2] ]
 
-      const res = await createVideoPlaylist({
-        url: servers[0].url,
-        token: userAccessTokenServer1,
-        playlistAttrs: {
+      const playlist = await commands[0].create({
+        token: userTokenServer1,
+        attributes: {
           displayName: 'playlist 56',
           privacy: VideoPlaylistPrivacy.PUBLIC,
-          videoChannelId: servers[0].videoChannel.id
+          videoChannelId: servers[0].store.channel.id
         }
       })
 
-      const playlistServer1Id2 = res.body.videoPlaylist.id
-      playlistServer1UUID2 = res.body.videoPlaylist.uuid
+      const playlistServer1Id2 = playlist.id
+      playlistServer1UUID2 = playlist.uuid
 
-      const addVideo = (elementAttrs: any) => {
-        return addVideoInPlaylist({ url: servers[0].url, token: userAccessTokenServer1, playlistId: playlistServer1Id2, elementAttrs })
+      const addVideo = (attributes: any) => {
+        return commands[0].addElement({ token: userTokenServer1, playlistId: playlistServer1Id2, attributes })
       }
 
-      video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 89', token: userAccessTokenServer1 })).uuid
-      video2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 90' })).uuid
-      video3 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 91', nsfw: true })).uuid
+      video1 = (await servers[0].videos.quickUpload({ name: 'video 89', token: userTokenServer1 })).uuid
+      video2 = (await servers[1].videos.quickUpload({ name: 'video 90' })).uuid
+      video3 = (await servers[0].videos.quickUpload({ name: 'video 91', nsfw: true })).uuid
 
       await waitJobs(servers)
 
@@ -707,7 +637,7 @@ describe('Test video playlists', function () {
       const position = 1
 
       {
-        await updateVideo(servers[0].url, servers[0].accessToken, video1, { privacy: VideoPrivacy.PRIVATE })
+        await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PRIVATE } })
         await waitJobs(servers)
 
         await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
@@ -717,7 +647,7 @@ describe('Test video playlists', function () {
       }
 
       {
-        await updateVideo(servers[0].url, servers[0].accessToken, video1, { privacy: VideoPrivacy.PUBLIC })
+        await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PUBLIC } })
         await waitJobs(servers)
 
         await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
@@ -735,7 +665,7 @@ describe('Test video playlists', function () {
       const position = 1
 
       {
-        await addVideoToBlacklist(servers[0].url, servers[0].accessToken, video1, 'reason', true)
+        await servers[0].blacklist.add({ videoId: video1, reason: 'reason', unfederate: true })
         await waitJobs(servers)
 
         await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
@@ -745,7 +675,7 @@ describe('Test video playlists', function () {
       }
 
       {
-        await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, video1)
+        await servers[0].blacklist.remove({ videoId: video1 })
         await waitJobs(servers)
 
         await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
@@ -759,56 +689,58 @@ describe('Test video playlists', function () {
     it('Should update the element type if the account or server of the video is blocked', async function () {
       this.timeout(90000)
 
+      const command = servers[0].blocklist
+
       const name = 'video 90'
       const position = 2
 
       {
-        await addAccountToAccountBlocklist(servers[0].url, userAccessTokenServer1, 'root@localhost:' + servers[1].port)
+        await command.addToMyBlocklist({ token: userTokenServer1, account: 'root@localhost:' + servers[1].port })
         await waitJobs(servers)
 
         await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
         await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
 
-        await removeAccountFromAccountBlocklist(servers[0].url, userAccessTokenServer1, 'root@localhost:' + servers[1].port)
+        await command.removeFromMyBlocklist({ token: userTokenServer1, account: 'root@localhost:' + servers[1].port })
         await waitJobs(servers)
 
         await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
       }
 
       {
-        await addServerToAccountBlocklist(servers[0].url, userAccessTokenServer1, 'localhost:' + servers[1].port)
+        await command.addToMyBlocklist({ token: userTokenServer1, server: 'localhost:' + servers[1].port })
         await waitJobs(servers)
 
         await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
         await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
 
-        await removeServerFromAccountBlocklist(servers[0].url, userAccessTokenServer1, 'localhost:' + servers[1].port)
+        await command.removeFromMyBlocklist({ token: userTokenServer1, server: 'localhost:' + servers[1].port })
         await waitJobs(servers)
 
         await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
       }
 
       {
-        await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'root@localhost:' + servers[1].port)
+        await command.addToServerBlocklist({ account: 'root@localhost:' + servers[1].port })
         await waitJobs(servers)
 
         await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
         await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
 
-        await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, 'root@localhost:' + servers[1].port)
+        await command.removeFromServerBlocklist({ account: 'root@localhost:' + servers[1].port })
         await waitJobs(servers)
 
         await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
       }
 
       {
-        await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
+        await command.addToServerBlocklist({ server: 'localhost:' + servers[1].port })
         await waitJobs(servers)
 
         await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
         await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
 
-        await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
+        await command.removeFromServerBlocklist({ server: 'localhost:' + servers[1].port })
         await waitJobs(servers)
 
         await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
@@ -816,10 +748,10 @@ describe('Test video playlists', function () {
     })
 
     it('Should hide the video if it is NSFW', async function () {
-      const res = await getPlaylistVideos(servers[0].url, userAccessTokenServer1, playlistServer1UUID2, 0, 10, { nsfw: false })
-      expect(res.body.total).to.equal(3)
+      const body = await commands[0].listVideos({ token: userTokenServer1, playlistId: playlistServer1UUID2, query: { nsfw: 'false' } })
+      expect(body.total).to.equal(3)
 
-      const elements: VideoPlaylistElement[] = res.body.data
+      const elements = body.data
       const element = elements.find(e => e.position === 3)
 
       expect(element).to.exist
@@ -835,11 +767,9 @@ describe('Test video playlists', function () {
       this.timeout(30000)
 
       {
-        await reorderVideosPlaylist({
-          url: servers[0].url,
-          token: servers[0].accessToken,
+        await commands[0].reorderElements({
           playlistId: playlistServer1Id,
-          elementAttrs: {
+          attributes: {
             startPosition: 2,
             insertAfterPosition: 3
           }
@@ -848,8 +778,8 @@ describe('Test video playlists', function () {
         await waitJobs(servers)
 
         for (const server of servers) {
-          const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
-          const names = (res.body.data as VideoPlaylistElement[]).map(v => v.video.name)
+          const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
+          const names = body.data.map(v => v.video.name)
 
           expect(names).to.deep.equal([
             'video 0 server 1',
@@ -865,11 +795,9 @@ describe('Test video playlists', function () {
       }
 
       {
-        await reorderVideosPlaylist({
-          url: servers[0].url,
-          token: servers[0].accessToken,
+        await commands[0].reorderElements({
           playlistId: playlistServer1Id,
-          elementAttrs: {
+          attributes: {
             startPosition: 1,
             reorderLength: 3,
             insertAfterPosition: 4
@@ -879,8 +807,8 @@ describe('Test video playlists', function () {
         await waitJobs(servers)
 
         for (const server of servers) {
-          const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
-          const names = (res.body.data as VideoPlaylistElement[]).map(v => v.video.name)
+          const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
+          const names = body.data.map(v => v.video.name)
 
           expect(names).to.deep.equal([
             'video 3 server 1',
@@ -896,11 +824,9 @@ describe('Test video playlists', function () {
       }
 
       {
-        await reorderVideosPlaylist({
-          url: servers[0].url,
-          token: servers[0].accessToken,
+        await commands[0].reorderElements({
           playlistId: playlistServer1Id,
-          elementAttrs: {
+          attributes: {
             startPosition: 6,
             insertAfterPosition: 3
           }
@@ -909,8 +835,7 @@ describe('Test video playlists', function () {
         await waitJobs(servers)
 
         for (const server of servers) {
-          const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
-          const elements: VideoPlaylistElement[] = res.body.data
+          const { data: elements } = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
           const names = elements.map(v => v.video.name)
 
           expect(names).to.deep.equal([
@@ -934,22 +859,18 @@ describe('Test video playlists', function () {
     it('Should update startTimestamp/endTimestamp of some elements', async function () {
       this.timeout(30000)
 
-      await updateVideoPlaylistElement({
-        url: servers[0].url,
-        token: servers[0].accessToken,
+      await commands[0].updateElement({
         playlistId: playlistServer1Id,
-        playlistElementId: playlistElementServer1Video4,
-        elementAttrs: {
+        elementId: playlistElementServer1Video4,
+        attributes: {
           startTimestamp: 1
         }
       })
 
-      await updateVideoPlaylistElement({
-        url: servers[0].url,
-        token: servers[0].accessToken,
+      await commands[0].updateElement({
         playlistId: playlistServer1Id,
-        playlistElementId: playlistElementServer1Video5,
-        elementAttrs: {
+        elementId: playlistElementServer1Video5,
+        attributes: {
           stopTimestamp: null
         }
       })
@@ -957,8 +878,7 @@ describe('Test video playlists', function () {
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
-        const elements: VideoPlaylistElement[] = res.body.data
+        const { data: elements } = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
 
         expect(elements[0].video.name).to.equal('video 3 server 1')
         expect(elements[0].position).to.equal(1)
@@ -974,17 +894,16 @@ describe('Test video playlists', function () {
 
     it('Should check videos existence in my playlist', async function () {
       const videoIds = [
-        servers[0].videos[0].id,
+        servers[0].store.videos[0].id,
         42000,
-        servers[0].videos[3].id,
+        servers[0].store.videos[3].id,
         43000,
-        servers[0].videos[4].id
+        servers[0].store.videos[4].id
       ]
-      const res = await doVideosExistInMyPlaylist(servers[0].url, servers[0].accessToken, videoIds)
-      const obj = res.body as VideoExistInPlaylist
+      const obj = await commands[0].videosExist({ videoIds })
 
       {
-        const elem = obj[servers[0].videos[0].id]
+        const elem = obj[servers[0].store.videos[0].id]
         expect(elem).to.have.lengthOf(1)
         expect(elem[0].playlistElementId).to.exist
         expect(elem[0].playlistId).to.equal(playlistServer1Id)
@@ -993,7 +912,7 @@ describe('Test video playlists', function () {
       }
 
       {
-        const elem = obj[servers[0].videos[3].id]
+        const elem = obj[servers[0].store.videos[3].id]
         expect(elem).to.have.lengthOf(1)
         expect(elem[0].playlistElementId).to.equal(playlistElementServer1Video4)
         expect(elem[0].playlistId).to.equal(playlistServer1Id)
@@ -1002,7 +921,7 @@ describe('Test video playlists', function () {
       }
 
       {
-        const elem = obj[servers[0].videos[4].id]
+        const elem = obj[servers[0].store.videos[4].id]
         expect(elem).to.have.lengthOf(1)
         expect(elem[0].playlistId).to.equal(playlistServer1Id)
         expect(elem[0].startTimestamp).to.equal(45)
@@ -1015,42 +934,29 @@ describe('Test video playlists', function () {
 
     it('Should automatically update updatedAt field of playlists', async function () {
       const server = servers[1]
-      const videoId = servers[1].videos[5].id
+      const videoId = servers[1].store.videos[5].id
 
       async function getPlaylistNames () {
-        const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root', 0, 5, undefined, '-updatedAt')
+        const { data } = await server.playlists.listByAccount({ token: server.accessToken, handle: 'root', sort: '-updatedAt' })
 
-        return (res.body.data as VideoPlaylist[]).map(p => p.displayName)
+        return data.map(p => p.displayName)
       }
 
-      const elementAttrs = { videoId }
-      const res1 = await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, elementAttrs })
-      const res2 = await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, elementAttrs })
-
-      const element1 = res1.body.videoPlaylistElement.id
-      const element2 = res2.body.videoPlaylistElement.id
+      const attributes = { videoId }
+      const element1 = await server.playlists.addElement({ playlistId: playlistServer2Id1, attributes })
+      const element2 = await server.playlists.addElement({ playlistId: playlistServer2Id2, attributes })
 
       const names1 = await getPlaylistNames()
       expect(names1[0]).to.equal('playlist 3 updated')
       expect(names1[1]).to.equal('playlist 2')
 
-      await removeVideoFromPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistServer2Id1,
-        playlistElementId: element1
-      })
+      await server.playlists.removeElement({ playlistId: playlistServer2Id1, elementId: element1.id })
 
       const names2 = await getPlaylistNames()
       expect(names2[0]).to.equal('playlist 2')
       expect(names2[1]).to.equal('playlist 3 updated')
 
-      await removeVideoFromPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistServer2Id2,
-        playlistElementId: element2
-      })
+      await server.playlists.removeElement({ playlistId: playlistServer2Id2, elementId: element2.id })
 
       const names3 = await getPlaylistNames()
       expect(names3[0]).to.equal('playlist 3 updated')
@@ -1060,28 +966,16 @@ describe('Test video playlists', function () {
     it('Should delete some elements', async function () {
       this.timeout(30000)
 
-      await removeVideoFromPlaylist({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        playlistId: playlistServer1Id,
-        playlistElementId: playlistElementServer1Video4
-      })
-
-      await removeVideoFromPlaylist({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        playlistId: playlistServer1Id,
-        playlistElementId: playlistElementNSFW
-      })
+      await commands[0].removeElement({ playlistId: playlistServer1Id, elementId: playlistElementServer1Video4 })
+      await commands[0].removeElement({ playlistId: playlistServer1Id, elementId: playlistElementNSFW })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
+        const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
+        expect(body.total).to.equal(6)
 
-        expect(res.body.total).to.equal(6)
-
-        const elements: VideoPlaylistElement[] = res.body.data
+        const elements = body.data
         expect(elements).to.have.lengthOf(6)
 
         expect(elements[0].video.name).to.equal('video 0 server 1')
@@ -1107,34 +1001,31 @@ describe('Test video playlists', function () {
     it('Should be able to create a public playlist, and set it to private', async function () {
       this.timeout(30000)
 
-      const res = await createVideoPlaylist({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        playlistAttrs: {
+      const videoPlaylistIds = await commands[0].create({
+        attributes: {
           displayName: 'my super public playlist',
           privacy: VideoPlaylistPrivacy.PUBLIC,
-          videoChannelId: servers[0].videoChannel.id
+          videoChannelId: servers[0].store.channel.id
         }
       })
-      const videoPlaylistIds = res.body.videoPlaylist
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        await getVideoPlaylist(server.url, videoPlaylistIds.uuid, HttpStatusCode.OK_200)
+        await server.playlists.get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.OK_200 })
       }
 
-      const playlistAttrs = { privacy: VideoPlaylistPrivacy.PRIVATE }
-      await updateVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: videoPlaylistIds.id, playlistAttrs })
+      const attributes = { privacy: VideoPlaylistPrivacy.PRIVATE }
+      await commands[0].update({ playlistId: videoPlaylistIds.id, attributes })
 
       await waitJobs(servers)
 
       for (const server of [ servers[1], servers[2] ]) {
-        await getVideoPlaylist(server.url, videoPlaylistIds.uuid, HttpStatusCode.NOT_FOUND_404)
+        await server.playlists.get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
       }
-      await getVideoPlaylist(servers[0].url, videoPlaylistIds.uuid, HttpStatusCode.UNAUTHORIZED_401)
 
-      await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistIds.uuid, HttpStatusCode.OK_200)
+      await commands[0].get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      await commands[0].get({ token: servers[0].accessToken, playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.OK_200 })
     })
   })
 
@@ -1143,12 +1034,12 @@ describe('Test video playlists', function () {
     it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () {
       this.timeout(30000)
 
-      await deleteVideoPlaylist(servers[0].url, servers[0].accessToken, playlistServer1Id)
+      await commands[0].delete({ playlistId: playlistServer1Id })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        await getVideoPlaylist(server.url, playlistServer1UUID, HttpStatusCode.NOT_FOUND_404)
+        await server.playlists.get({ playlistId: playlistServer1UUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
       }
     })
 
@@ -1163,75 +1054,61 @@ describe('Test video playlists', function () {
     it('Should unfollow servers 1 and 2 and hide their playlists', async function () {
       this.timeout(30000)
 
-      const finder = data => data.find(p => p.displayName === 'my super playlist')
+      const finder = (data: VideoPlaylist[]) => data.find(p => p.displayName === 'my super playlist')
 
       {
-        const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
-        expect(res.body.total).to.equal(3)
-        expect(finder(res.body.data)).to.not.be.undefined
+        const body = await servers[2].playlists.list({ start: 0, count: 5 })
+        expect(body.total).to.equal(3)
+
+        expect(finder(body.data)).to.not.be.undefined
       }
 
-      await unfollow(servers[2].url, servers[2].accessToken, servers[0])
+      await servers[2].follows.unfollow({ target: servers[0] })
 
       {
-        const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
-        expect(res.body.total).to.equal(1)
+        const body = await servers[2].playlists.list({ start: 0, count: 5 })
+        expect(body.total).to.equal(1)
 
-        expect(finder(res.body.data)).to.be.undefined
+        expect(finder(body.data)).to.be.undefined
       }
     })
 
     it('Should delete a channel and put the associated playlist in private mode', async function () {
       this.timeout(30000)
 
-      const res = await addVideoChannel(servers[0].url, servers[0].accessToken, { name: 'super_channel', displayName: 'super channel' })
-      const videoChannelId = res.body.videoChannel.id
+      const channel = await servers[0].channels.create({ attributes: { name: 'super_channel', displayName: 'super channel' } })
 
-      const res2 = await createVideoPlaylist({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        playlistAttrs: {
+      const playlistCreated = await commands[0].create({
+        attributes: {
           displayName: 'channel playlist',
           privacy: VideoPlaylistPrivacy.PUBLIC,
-          videoChannelId
+          videoChannelId: channel.id
         }
       })
-      const videoPlaylistUUID = res2.body.videoPlaylist.uuid
 
       await waitJobs(servers)
 
-      await deleteVideoChannel(servers[0].url, servers[0].accessToken, 'super_channel')
+      await servers[0].channels.delete({ channelName: 'super_channel' })
 
       await waitJobs(servers)
 
-      const res3 = await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistUUID)
-      expect(res3.body.displayName).to.equal('channel playlist')
-      expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
+      const body = await commands[0].get({ token: servers[0].accessToken, playlistId: playlistCreated.uuid })
+      expect(body.displayName).to.equal('channel playlist')
+      expect(body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
 
-      await getVideoPlaylist(servers[1].url, videoPlaylistUUID, HttpStatusCode.NOT_FOUND_404)
+      await servers[1].playlists.get({ playlistId: playlistCreated.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should delete an account and delete its playlists', async function () {
       this.timeout(30000)
 
-      const user = { username: 'user_1', password: 'password' }
-      const res = await createUser({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
-        username: user.username,
-        password: user.password
-      })
-
-      const userId = res.body.user.id
-      const userAccessToken = await userLogin(servers[0], user)
+      const { userId, token } = await servers[0].users.generate('user_1')
 
-      const resChannel = await getMyUserInformation(servers[0].url, userAccessToken)
-      const userChannel = (resChannel.body as User).videoChannels[0]
+      const { videoChannels } = await servers[0].users.getMyInfo({ token })
+      const userChannel = videoChannels[0]
 
-      await createVideoPlaylist({
-        url: servers[0].url,
-        token: userAccessToken,
-        playlistAttrs: {
+      await commands[0].create({
+        attributes: {
           displayName: 'playlist to be deleted',
           privacy: VideoPlaylistPrivacy.PUBLIC,
           videoChannelId: userChannel.id
@@ -1240,22 +1117,24 @@ describe('Test video playlists', function () {
 
       await waitJobs(servers)
 
-      const finder = data => data.find(p => p.displayName === 'playlist to be deleted')
+      const finder = (data: VideoPlaylist[]) => data.find(p => p.displayName === 'playlist to be deleted')
 
       {
         for (const server of [ servers[0], servers[1] ]) {
-          const res = await getVideoPlaylistsList(server.url, 0, 15)
-          expect(finder(res.body.data)).to.not.be.undefined
+          const body = await server.playlists.list({ start: 0, count: 15 })
+
+          expect(finder(body.data)).to.not.be.undefined
         }
       }
 
-      await removeUser(servers[0].url, userId, servers[0].accessToken)
+      await servers[0].users.remove({ userId })
       await waitJobs(servers)
 
       {
         for (const server of [ servers[0], servers[1] ]) {
-          const res = await getVideoPlaylistsList(server.url, 0, 15)
-          expect(finder(res.body.data)).to.be.undefined
+          const body = await server.playlists.list({ start: 0, count: 15 })
+
+          expect(finder(body.data)).to.be.undefined
         }
       }
     })
index 950aeb7cf713a223c0349c9c68f3661cf0c8b5c7..b51b3bcddb58507e4fbf8ded8e3435be50c3c1cc 100644 (file)
@@ -2,28 +2,13 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { Video, VideoCreateResult } from '@shared/models'
-import {
-  cleanupTests,
-  flushAndRunServer,
-  getVideosList,
-  getVideosListWithToken,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo
-} from '../../../../shared/extra-utils/index'
-import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { userLogin } from '../../../../shared/extra-utils/users/login'
-import { createUser } from '../../../../shared/extra-utils/users/users'
-import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/extra-utils/videos/videos'
-import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
+import { cleanupTests, createSingleServer, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
+import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test video privacy', function () {
-  const servers: ServerInfo[] = []
+  const servers: PeerTubeServer[] = []
   let anotherUserToken: string
 
   let privateVideoId: number
@@ -49,8 +34,8 @@ describe('Test video privacy', function () {
     this.timeout(50000)
 
     // Run servers
-    servers.push(await flushAndRunServer(1, dontFederateUnlistedConfig))
-    servers.push(await flushAndRunServer(2))
+    servers.push(await createSingleServer(1, dontFederateUnlistedConfig))
+    servers.push(await createSingleServer(2))
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
@@ -66,55 +51,53 @@ describe('Test video privacy', function () {
 
       for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) {
         const attributes = { privacy }
-        await uploadVideo(servers[0].url, servers[0].accessToken, attributes)
+        await servers[0].videos.upload({ attributes })
       }
 
       await waitJobs(servers)
     })
 
     it('Should not have these private and internal videos on server 2', async function () {
-      const res = await getVideosList(servers[1].url)
+      const { total, data } = await servers[1].videos.list()
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(total).to.equal(0)
+      expect(data).to.have.lengthOf(0)
     })
 
     it('Should not list the private and internal videos for an unauthenticated user on server 1', async function () {
-      const res = await getVideosList(servers[0].url)
+      const { total, data } = await servers[0].videos.list()
 
-      expect(res.body.total).to.equal(0)
-      expect(res.body.data).to.have.lengthOf(0)
+      expect(total).to.equal(0)
+      expect(data).to.have.lengthOf(0)
     })
 
     it('Should not list the private video and list the internal video for an authenticated user on server 1', async function () {
-      const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
+      const { total, data } = await servers[0].videos.listWithToken()
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
+      expect(total).to.equal(1)
+      expect(data).to.have.lengthOf(1)
 
-      expect(res.body.data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL)
+      expect(data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL)
     })
 
     it('Should list my (private and internal) videos', async function () {
-      const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 10)
+      const { total, data } = await servers[0].videos.listMyVideos()
 
-      expect(res.body.total).to.equal(2)
-      expect(res.body.data).to.have.lengthOf(2)
+      expect(total).to.equal(2)
+      expect(data).to.have.lengthOf(2)
 
-      const videos: Video[] = res.body.data
-
-      const privateVideo = videos.find(v => v.privacy.id === VideoPrivacy.PRIVATE)
+      const privateVideo = data.find(v => v.privacy.id === VideoPrivacy.PRIVATE)
       privateVideoId = privateVideo.id
       privateVideoUUID = privateVideo.uuid
 
-      const internalVideo = videos.find(v => v.privacy.id === VideoPrivacy.INTERNAL)
+      const internalVideo = data.find(v => v.privacy.id === VideoPrivacy.INTERNAL)
       internalVideoId = internalVideo.id
       internalVideoUUID = internalVideo.uuid
     })
 
     it('Should not be able to watch the private/internal video with non authenticated user', async function () {
-      await getVideo(servers[0].url, privateVideoUUID, HttpStatusCode.UNAUTHORIZED_401)
-      await getVideo(servers[0].url, internalVideoUUID, HttpStatusCode.UNAUTHORIZED_401)
+      await servers[0].videos.get({ id: privateVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      await servers[0].videos.get({ id: internalVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
 
     it('Should not be able to watch the private video with another user', async function () {
@@ -124,18 +107,23 @@ describe('Test video privacy', function () {
         username: 'hello',
         password: 'super password'
       }
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
+      await servers[0].users.create({ username: user.username, password: user.password })
+
+      anotherUserToken = await servers[0].login.getAccessToken(user)
 
-      anotherUserToken = await userLogin(servers[0], user)
-      await getVideoWithToken(servers[0].url, anotherUserToken, privateVideoUUID, HttpStatusCode.FORBIDDEN_403)
+      await servers[0].videos.getWithToken({
+        token: anotherUserToken,
+        id: privateVideoUUID,
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
     })
 
     it('Should be able to watch the internal video with another user', async function () {
-      await getVideoWithToken(servers[0].url, anotherUserToken, internalVideoUUID, HttpStatusCode.OK_200)
+      await servers[0].videos.getWithToken({ token: anotherUserToken, id: internalVideoUUID })
     })
 
     it('Should be able to watch the private video with the correct user', async function () {
-      await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID, HttpStatusCode.OK_200)
+      await servers[0].videos.getWithToken({ id: privateVideoUUID })
     })
   })
 
@@ -148,7 +136,7 @@ describe('Test video privacy', function () {
         name: 'unlisted video',
         privacy: VideoPrivacy.UNLISTED
       }
-      await uploadVideo(servers[1].url, servers[1].accessToken, attributes)
+      await servers[1].videos.upload({ attributes })
 
       // Server 2 has transcoding enabled
       await waitJobs(servers)
@@ -156,32 +144,32 @@ describe('Test video privacy', function () {
 
     it('Should not have this unlisted video listed on server 1 and 2', async function () {
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { total, data } = await server.videos.list()
 
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
+        expect(total).to.equal(0)
+        expect(data).to.have.lengthOf(0)
       }
     })
 
     it('Should list my (unlisted) videos', async function () {
-      const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 1)
+      const { total, data } = await servers[1].videos.listMyVideos()
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
+      expect(total).to.equal(1)
+      expect(data).to.have.lengthOf(1)
 
-      unlistedVideo = res.body.data[0]
+      unlistedVideo = data[0]
     })
 
     it('Should not be able to get this unlisted video using its id', async function () {
-      await getVideo(servers[1].url, unlistedVideo.id, 404)
+      await servers[1].videos.get({ id: unlistedVideo.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
 
     it('Should be able to get this unlisted video using its uuid/shortUUID', async function () {
       for (const server of servers) {
         for (const id of [ unlistedVideo.uuid, unlistedVideo.shortUUID ]) {
-          const res = await getVideo(server.url, id)
+          const video = await server.videos.get({ id })
 
-          expect(res.body.name).to.equal('unlisted video')
+          expect(video.name).to.equal('unlisted video')
         }
       }
     })
@@ -193,28 +181,28 @@ describe('Test video privacy', function () {
         name: 'unlisted video',
         privacy: VideoPrivacy.UNLISTED
       }
-      await uploadVideo(servers[0].url, servers[0].accessToken, attributes)
+      await servers[0].videos.upload({ attributes })
 
       await waitJobs(servers)
     })
 
     it('Should list my new unlisted video', async function () {
-      const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 3)
+      const { total, data } = await servers[0].videos.listMyVideos()
 
-      expect(res.body.total).to.equal(3)
-      expect(res.body.data).to.have.lengthOf(3)
+      expect(total).to.equal(3)
+      expect(data).to.have.lengthOf(3)
 
-      nonFederatedUnlistedVideoUUID = res.body.data[0].uuid
+      nonFederatedUnlistedVideoUUID = data[0].uuid
     })
 
     it('Should be able to get non-federated unlisted video from origin', async function () {
-      const res = await getVideo(servers[0].url, nonFederatedUnlistedVideoUUID)
+      const video = await servers[0].videos.get({ id: nonFederatedUnlistedVideoUUID })
 
-      expect(res.body.name).to.equal('unlisted video')
+      expect(video.name).to.equal('unlisted video')
     })
 
     it('Should not be able to get non-federated unlisted video from federated server', async function () {
-      await getVideo(servers[1].url, nonFederatedUnlistedVideoUUID, HttpStatusCode.NOT_FOUND_404)
+      await servers[1].videos.get({ id: nonFederatedUnlistedVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
     })
   })
 
@@ -226,20 +214,20 @@ describe('Test video privacy', function () {
       now = Date.now()
 
       {
-        const attribute = {
+        const attributes = {
           name: 'private video becomes public',
           privacy: VideoPrivacy.PUBLIC
         }
 
-        await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, attribute)
+        await servers[0].videos.update({ id: privateVideoId, attributes })
       }
 
       {
-        const attribute = {
+        const attributes = {
           name: 'internal video becomes public',
           privacy: VideoPrivacy.PUBLIC
         }
-        await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, attribute)
+        await servers[0].videos.update({ id: internalVideoId, attributes })
       }
 
       await waitJobs(servers)
@@ -247,13 +235,12 @@ describe('Test video privacy', function () {
 
     it('Should have this new public video listed on server 1 and 2', async function () {
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-        expect(res.body.total).to.equal(2)
-        expect(res.body.data).to.have.lengthOf(2)
+        const { total, data } = await server.videos.list()
+        expect(total).to.equal(2)
+        expect(data).to.have.lengthOf(2)
 
-        const videos: Video[] = res.body.data
-        const privateVideo = videos.find(v => v.name === 'private video becomes public')
-        const internalVideo = videos.find(v => v.name === 'internal video becomes public')
+        const privateVideo = data.find(v => v.name === 'private video becomes public')
+        const internalVideo = data.find(v => v.name === 'internal video becomes public')
 
         expect(privateVideo).to.not.be.undefined
         expect(internalVideo).to.not.be.undefined
@@ -270,27 +257,25 @@ describe('Test video privacy', function () {
     it('Should set these videos as private and internal', async function () {
       this.timeout(10000)
 
-      await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, { privacy: VideoPrivacy.PRIVATE })
-      await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.INTERNAL })
+      await servers[0].videos.update({ id: internalVideoId, attributes: { privacy: VideoPrivacy.PRIVATE } })
+      await servers[0].videos.update({ id: privateVideoId, attributes: { privacy: VideoPrivacy.INTERNAL } })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { total, data } = await server.videos.list()
 
-        expect(res.body.total).to.equal(0)
-        expect(res.body.data).to.have.lengthOf(0)
+        expect(total).to.equal(0)
+        expect(data).to.have.lengthOf(0)
       }
 
       {
-        const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
-        const videos = res.body.data
-
-        expect(res.body.total).to.equal(3)
-        expect(videos).to.have.lengthOf(3)
+        const { total, data } = await servers[0].videos.listMyVideos()
+        expect(total).to.equal(3)
+        expect(data).to.have.lengthOf(3)
 
-        const privateVideo = videos.find(v => v.name === 'private video becomes public')
-        const internalVideo = videos.find(v => v.name === 'internal video becomes public')
+        const privateVideo = data.find(v => v.name === 'private video becomes public')
+        const internalVideo = data.find(v => v.name === 'internal video becomes public')
 
         expect(privateVideo).to.not.be.undefined
         expect(internalVideo).to.not.be.undefined
index 204f436116199b2295cba3d79d60aa15dff14174..3f77387842d1455f0441be17043ebc6d2bf4ebb9 100644 (file)
@@ -1,22 +1,17 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { VideoPrivacy } from '../../../../shared/models/videos'
+import * as chai from 'chai'
 import {
   cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getMyVideos,
-  getVideosList,
-  getVideoWithToken,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updateVideo,
-  uploadVideo,
-  wait
-} from '../../../../shared/extra-utils'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
@@ -28,14 +23,14 @@ function in10Seconds () {
 }
 
 describe('Test video update scheduler', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let video2UUID: string
 
   before(async function () {
     this.timeout(30000)
 
     // Run servers
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
 
@@ -45,35 +40,34 @@ describe('Test video update scheduler', function () {
   it('Should upload a video and schedule an update in 10 seconds', async function () {
     this.timeout(10000)
 
-    const videoAttributes = {
+    const attributes = {
       name: 'video 1',
       privacy: VideoPrivacy.PRIVATE,
       scheduleUpdate: {
         updateAt: in10Seconds().toISOString(),
-        privacy: VideoPrivacy.PUBLIC
+        privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC
       }
     }
 
-    await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
+    await servers[0].videos.upload({ attributes })
 
     await waitJobs(servers)
   })
 
   it('Should not list the video (in privacy mode)', async function () {
     for (const server of servers) {
-      const res = await getVideosList(server.url)
+      const { total } = await server.videos.list()
 
-      expect(res.body.total).to.equal(0)
+      expect(total).to.equal(0)
     }
   })
 
   it('Should have my scheduled video in my account videos', async function () {
-    const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
-    expect(res.body.total).to.equal(1)
+    const { total, data } = await servers[0].videos.listMyVideos()
+    expect(total).to.equal(1)
 
-    const videoFromList = res.body.data[0]
-    const res2 = await getVideoWithToken(servers[0].url, servers[0].accessToken, videoFromList.uuid)
-    const videoFromGet = res2.body
+    const videoFromList = data[0]
+    const videoFromGet = await servers[0].videos.getWithToken({ id: videoFromList.uuid })
 
     for (const video of [ videoFromList, videoFromGet ]) {
       expect(video.name).to.equal('video 1')
@@ -90,23 +84,23 @@ describe('Test video update scheduler', function () {
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
+      const { total, data } = await server.videos.list()
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data[0].name).to.equal('video 1')
+      expect(total).to.equal(1)
+      expect(data[0].name).to.equal('video 1')
     }
   })
 
   it('Should upload a video without scheduling an update', async function () {
     this.timeout(10000)
 
-    const videoAttributes = {
+    const attributes = {
       name: 'video 2',
       privacy: VideoPrivacy.PRIVATE
     }
 
-    const res = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
-    video2UUID = res.body.video.uuid
+    const { uuid } = await servers[0].videos.upload({ attributes })
+    video2UUID = uuid
 
     await waitJobs(servers)
   })
@@ -114,31 +108,31 @@ describe('Test video update scheduler', function () {
   it('Should update a video by scheduling an update', async function () {
     this.timeout(10000)
 
-    const videoAttributes = {
+    const attributes = {
       name: 'video 2 updated',
       scheduleUpdate: {
         updateAt: in10Seconds().toISOString(),
-        privacy: VideoPrivacy.PUBLIC
+        privacy: VideoPrivacy.PUBLIC as VideoPrivacy.PUBLIC
       }
     }
 
-    await updateVideo(servers[0].url, servers[0].accessToken, video2UUID, videoAttributes)
+    await servers[0].videos.update({ id: video2UUID, attributes })
     await waitJobs(servers)
   })
 
   it('Should not display the updated video', async function () {
     for (const server of servers) {
-      const res = await getVideosList(server.url)
+      const { total } = await server.videos.list()
 
-      expect(res.body.total).to.equal(1)
+      expect(total).to.equal(1)
     }
   })
 
   it('Should have my scheduled updated video in my account videos', async function () {
-    const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
-    expect(res.body.total).to.equal(2)
+    const { total, data } = await servers[0].videos.listMyVideos()
+    expect(total).to.equal(2)
 
-    const video = res.body.data.find(v => v.uuid === video2UUID)
+    const video = data.find(v => v.uuid === video2UUID)
     expect(video).not.to.be.undefined
 
     expect(video.name).to.equal('video 2 updated')
@@ -155,11 +149,10 @@ describe('Test video update scheduler', function () {
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-
-      expect(res.body.total).to.equal(2)
+      const { total, data } = await server.videos.list()
+      expect(total).to.equal(2)
 
-      const video = res.body.data.find(v => v.uuid === video2UUID)
+      const video = data.find(v => v.uuid === video2UUID)
       expect(video).not.to.be.undefined
       expect(video.name).to.equal('video 2 updated')
     }
index ea5ffd23917c3f0636055c7babba5f7603bcee1e..2a09e95bf98e75a50600eaba042be61943f3603b 100644 (file)
@@ -2,36 +2,23 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { FfprobeData } from 'fluent-ffmpeg'
 import { omit } from 'lodash'
-import { join } from 'path'
-import { Job } from '@shared/models'
-import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   buildAbsoluteFixturePath,
-  buildServerDirectory,
   cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
   generateHighBitrateVideo,
   generateVideoWithFramerate,
-  getJobsListPaginationAndSort,
-  getMyVideos,
-  getServerFileSize,
-  getVideo,
-  getVideoFileMetadataUrl,
-  getVideosList,
+  getFileSize,
   makeGetRequest,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updateCustomSubConfig,
-  uploadVideo,
-  uploadVideoAndGetId,
   waitJobs,
   webtorrentAdd
-} from '../../../../shared/extra-utils'
-import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos'
+} from '@shared/extra-utils'
+import { getMaxBitrate, HttpStatusCode, VideoResolution, VideoState } from '@shared/models'
+import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
 import {
   canDoQuickTranscode,
   getAudioStream,
@@ -43,37 +30,39 @@ import {
 
 const expect = chai.expect
 
-function updateConfigForTranscoding (server: ServerInfo) {
-  return updateCustomSubConfig(server.url, server.accessToken, {
-    transcoding: {
-      enabled: true,
-      allowAdditionalExtensions: true,
-      allowAudioFiles: true,
-      hls: { enabled: true },
-      webtorrent: { enabled: true },
-      resolutions: {
-        '0p': false,
-        '240p': true,
-        '360p': true,
-        '480p': true,
-        '720p': true,
-        '1080p': true,
-        '1440p': true,
-        '2160p': true
+function updateConfigForTranscoding (server: PeerTubeServer) {
+  return server.config.updateCustomSubConfig({
+    newConfig: {
+      transcoding: {
+        enabled: true,
+        allowAdditionalExtensions: true,
+        allowAudioFiles: true,
+        hls: { enabled: true },
+        webtorrent: { enabled: true },
+        resolutions: {
+          '0p': false,
+          '240p': true,
+          '360p': true,
+          '480p': true,
+          '720p': true,
+          '1080p': true,
+          '1440p': true,
+          '2160p': true
+        }
       }
     }
   })
 }
 
 describe('Test video transcoding', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let video4k: string
 
   before(async function () {
     this.timeout(30_000)
 
     // Run servers
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
 
@@ -87,21 +76,20 @@ describe('Test video transcoding', function () {
     it('Should not transcode video on server 1', async function () {
       this.timeout(60_000)
 
-      const videoAttributes = {
+      const attributes = {
         name: 'my super name for server 1',
         description: 'my super description for server 1',
         fixture: 'video_short.webm'
       }
-      await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
+      await servers[0].videos.upload({ attributes })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-        const video = res.body.data[0]
+        const { data } = await server.videos.list()
+        const video = data[0]
 
-        const res2 = await getVideo(server.url, video.id)
-        const videoDetails = res2.body
+        const videoDetails = await server.videos.get({ id: video.id })
         expect(videoDetails.files).to.have.lengthOf(1)
 
         const magnetUri = videoDetails.files[0].magnetUri
@@ -117,21 +105,20 @@ describe('Test video transcoding', function () {
     it('Should transcode video on server 2', async function () {
       this.timeout(120_000)
 
-      const videoAttributes = {
+      const attributes = {
         name: 'my super name for server 2',
         description: 'my super description for server 2',
         fixture: 'video_short.webm'
       }
-      await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
+      await servers[1].videos.upload({ attributes })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        const video = res.body.data.find(v => v.name === videoAttributes.name)
-        const res2 = await getVideo(server.url, video.id)
-        const videoDetails = res2.body
+        const video = data.find(v => v.name === attributes.name)
+        const videoDetails = await server.videos.get({ id: video.id })
 
         expect(videoDetails.files).to.have.lengthOf(4)
 
@@ -150,47 +137,50 @@ describe('Test video transcoding', function () {
 
       {
         // Upload the video, but wait transcoding
-        const videoAttributes = {
+        const attributes = {
           name: 'waiting video',
           fixture: 'video_short1.webm',
           waitTranscoding: true
         }
-        const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
-        const videoId = resVideo.body.video.uuid
+        const { uuid } = await servers[1].videos.upload({ attributes })
+        const videoId = uuid
 
         // Should be in transcode state
-        const { body } = await getVideo(servers[1].url, videoId)
+        const body = await servers[1].videos.get({ id: videoId })
         expect(body.name).to.equal('waiting video')
         expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
         expect(body.state.label).to.equal('To transcode')
         expect(body.waitTranscoding).to.be.true
 
-        // Should have my video
-        const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
-        const videoToFindInMine = resMyVideos.body.data.find(v => v.name === videoAttributes.name)
-        expect(videoToFindInMine).not.to.be.undefined
-        expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
-        expect(videoToFindInMine.state.label).to.equal('To transcode')
-        expect(videoToFindInMine.waitTranscoding).to.be.true
+        {
+          // Should have my video
+          const { data } = await servers[1].videos.listMyVideos()
+          const videoToFindInMine = data.find(v => v.name === attributes.name)
+          expect(videoToFindInMine).not.to.be.undefined
+          expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
+          expect(videoToFindInMine.state.label).to.equal('To transcode')
+          expect(videoToFindInMine.waitTranscoding).to.be.true
+        }
 
-        // Should not list this video
-        const resVideos = await getVideosList(servers[1].url)
-        const videoToFindInList = resVideos.body.data.find(v => v.name === videoAttributes.name)
-        expect(videoToFindInList).to.be.undefined
+        {
+          // Should not list this video
+          const { data } = await servers[1].videos.list()
+          const videoToFindInList = data.find(v => v.name === attributes.name)
+          expect(videoToFindInList).to.be.undefined
+        }
 
         // Server 1 should not have the video yet
-        await getVideo(servers[0].url, videoId, HttpStatusCode.NOT_FOUND_404)
+        await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
       }
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-        const videoToFind = res.body.data.find(v => v.name === 'waiting video')
+        const { data } = await server.videos.list()
+        const videoToFind = data.find(v => v.name === 'waiting video')
         expect(videoToFind).not.to.be.undefined
 
-        const res2 = await getVideo(server.url, videoToFind.id)
-        const videoDetails: VideoDetails = res2.body
+        const videoDetails = await server.videos.get({ id: videoToFind.id })
 
         expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
         expect(videoDetails.state.label).to.equal('Published')
@@ -211,22 +201,20 @@ describe('Test video transcoding', function () {
       }
 
       for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) {
-        const videoAttributes = {
+        const attributes = {
           name: fixture,
           fixture
         }
 
-        await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
+        await servers[1].videos.upload({ attributes })
 
         await waitJobs(servers)
 
         for (const server of servers) {
-          const res = await getVideosList(server.url)
-
-          const video = res.body.data.find(v => v.name === videoAttributes.name)
-          const res2 = await getVideo(server.url, video.id)
-          const videoDetails = res2.body
+          const { data } = await server.videos.list()
 
+          const video = data.find(v => v.name === attributes.name)
+          const videoDetails = await server.videos.get({ id: video.id })
           expect(videoDetails.files).to.have.lengthOf(4)
 
           const magnetUri = videoDetails.files[0].magnetUri
@@ -238,22 +226,20 @@ describe('Test video transcoding', function () {
     it('Should transcode a 4k video', async function () {
       this.timeout(200_000)
 
-      const videoAttributes = {
+      const attributes = {
         name: '4k video',
         fixture: 'video_short_4k.mp4'
       }
 
-      const resUpload = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
-      video4k = resUpload.body.video.uuid
+      const { uuid } = await servers[1].videos.upload({ attributes })
+      video4k = uuid
 
       await waitJobs(servers)
 
       const resolutions = [ 240, 360, 480, 720, 1080, 1440, 2160 ]
 
       for (const server of servers) {
-        const res = await getVideo(server.url, video4k)
-        const videoDetails: VideoDetails = res.body
-
+        const videoDetails = await server.videos.get({ id: video4k })
         expect(videoDetails.files).to.have.lengthOf(resolutions.length)
 
         for (const r of resolutions) {
@@ -269,24 +255,24 @@ describe('Test video transcoding', function () {
     it('Should transcode high bit rate mp3 to proper bit rate', async function () {
       this.timeout(60_000)
 
-      const videoAttributes = {
+      const attributes = {
         name: 'mp3_256k',
         fixture: 'video_short_mp3_256k.mp4'
       }
-      await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
+      await servers[1].videos.upload({ attributes })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        const video = res.body.data.find(v => v.name === videoAttributes.name)
-        const res2 = await getVideo(server.url, video.id)
-        const videoDetails: VideoDetails = res2.body
+        const video = data.find(v => v.name === attributes.name)
+        const videoDetails = await server.videos.get({ id: video.id })
 
         expect(videoDetails.files).to.have.lengthOf(4)
 
-        const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
+        const file = videoDetails.files.find(f => f.resolution.id === 240)
+        const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
         const probe = await getAudioStream(path)
 
         if (probe.audioStream) {
@@ -301,23 +287,23 @@ describe('Test video transcoding', function () {
     it('Should transcode video with no audio and have no audio itself', async function () {
       this.timeout(60_000)
 
-      const videoAttributes = {
+      const attributes = {
         name: 'no_audio',
         fixture: 'video_short_no_audio.mp4'
       }
-      await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
+      await servers[1].videos.upload({ attributes })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        const video = res.body.data.find(v => v.name === videoAttributes.name)
-        const res2 = await getVideo(server.url, video.id)
-        const videoDetails: VideoDetails = res2.body
+        const video = data.find(v => v.name === attributes.name)
+        const videoDetails = await server.videos.get({ id: video.id })
+
+        const file = videoDetails.files.find(f => f.resolution.id === 240)
+        const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
 
-        expect(videoDetails.files).to.have.lengthOf(4)
-        const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
         const probe = await getAudioStream(path)
         expect(probe).to.not.have.property('audioStream')
       }
@@ -326,26 +312,27 @@ describe('Test video transcoding', function () {
     it('Should leave the audio untouched, but properly transcode the video', async function () {
       this.timeout(60_000)
 
-      const videoAttributes = {
+      const attributes = {
         name: 'untouched_audio',
         fixture: 'video_short.mp4'
       }
-      await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
+      await servers[1].videos.upload({ attributes })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        const video = res.body.data.find(v => v.name === videoAttributes.name)
-        const res2 = await getVideo(server.url, video.id)
-        const videoDetails: VideoDetails = res2.body
+        const video = data.find(v => v.name === attributes.name)
+        const videoDetails = await server.videos.get({ id: video.id })
 
         expect(videoDetails.files).to.have.lengthOf(4)
 
-        const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture)
+        const fixturePath = buildAbsoluteFixturePath(attributes.fixture)
         const fixtureVideoProbe = await getAudioStream(fixturePath)
-        const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
+
+        const file = videoDetails.files.find(f => f.resolution.id === 240)
+        const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
 
         const videoProbe = await getAudioStream(path)
 
@@ -364,19 +351,21 @@ describe('Test video transcoding', function () {
     function runSuite (mode: 'legacy' | 'resumable') {
 
       before(async function () {
-        await updateCustomSubConfig(servers[1].url, servers[1].accessToken, {
-          transcoding: {
-            hls: { enabled: true },
-            webtorrent: { enabled: true },
-            resolutions: {
-              '0p': false,
-              '240p': false,
-              '360p': false,
-              '480p': false,
-              '720p': false,
-              '1080p': false,
-              '1440p': false,
-              '2160p': false
+        await servers[1].config.updateCustomSubConfig({
+          newConfig: {
+            transcoding: {
+              hls: { enabled: true },
+              webtorrent: { enabled: true },
+              resolutions: {
+                '0p': false,
+                '240p': false,
+                '360p': false,
+                '480p': false,
+                '720p': false,
+                '1080p': false,
+                '1440p': false,
+                '2160p': false
+              }
             }
           }
         })
@@ -385,22 +374,21 @@ describe('Test video transcoding', function () {
       it('Should merge an audio file with the preview file', async function () {
         this.timeout(60_000)
 
-        const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
-        await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode)
+        const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
+        await servers[1].videos.upload({ attributes, mode })
 
         await waitJobs(servers)
 
         for (const server of servers) {
-          const res = await getVideosList(server.url)
+          const { data } = await server.videos.list()
 
-          const video = res.body.data.find(v => v.name === 'audio_with_preview')
-          const res2 = await getVideo(server.url, video.id)
-          const videoDetails: VideoDetails = res2.body
+          const video = data.find(v => v.name === 'audio_with_preview')
+          const videoDetails = await server.videos.get({ id: video.id })
 
           expect(videoDetails.files).to.have.lengthOf(1)
 
-          await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 })
-          await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 })
+          await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
+          await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 })
 
           const magnetUri = videoDetails.files[0].magnetUri
           expect(magnetUri).to.contain('.mp4')
@@ -410,22 +398,21 @@ describe('Test video transcoding', function () {
       it('Should upload an audio file and choose a default background image', async function () {
         this.timeout(60_000)
 
-        const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' }
-        await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode)
+        const attributes = { name: 'audio_without_preview', fixture: 'sample.ogg' }
+        await servers[1].videos.upload({ attributes, mode })
 
         await waitJobs(servers)
 
         for (const server of servers) {
-          const res = await getVideosList(server.url)
+          const { data } = await server.videos.list()
 
-          const video = res.body.data.find(v => v.name === 'audio_without_preview')
-          const res2 = await getVideo(server.url, video.id)
-          const videoDetails = res2.body
+          const video = data.find(v => v.name === 'audio_without_preview')
+          const videoDetails = await server.videos.get({ id: video.id })
 
           expect(videoDetails.files).to.have.lengthOf(1)
 
-          await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 })
-          await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 })
+          await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
+          await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 })
 
           const magnetUri = videoDetails.files[0].magnetUri
           expect(magnetUri).to.contain('.mp4')
@@ -435,26 +422,27 @@ describe('Test video transcoding', function () {
       it('Should upload an audio file and create an audio version only', async function () {
         this.timeout(60_000)
 
-        await updateCustomSubConfig(servers[1].url, servers[1].accessToken, {
-          transcoding: {
-            hls: { enabled: true },
-            webtorrent: { enabled: true },
-            resolutions: {
-              '0p': true,
-              '240p': false,
-              '360p': false
+        await servers[1].config.updateCustomSubConfig({
+          newConfig: {
+            transcoding: {
+              hls: { enabled: true },
+              webtorrent: { enabled: true },
+              resolutions: {
+                '0p': true,
+                '240p': false,
+                '360p': false
+              }
             }
           }
         })
 
-        const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
-        const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode)
+        const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
+        const { id } = await servers[1].videos.upload({ attributes, mode })
 
         await waitJobs(servers)
 
         for (const server of servers) {
-          const res2 = await getVideo(server.url, resVideo.body.video.id)
-          const videoDetails: VideoDetails = res2.body
+          const videoDetails = await server.videos.get({ id })
 
           for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) {
             expect(files).to.have.lengthOf(2)
@@ -480,21 +468,20 @@ describe('Test video transcoding', function () {
     it('Should transcode a 60 FPS video', async function () {
       this.timeout(60_000)
 
-      const videoAttributes = {
+      const attributes = {
         name: 'my super 30fps name for server 2',
         description: 'my super 30fps description for server 2',
         fixture: '60fps_720p_small.mp4'
       }
-      await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
+      await servers[1].videos.upload({ attributes })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        const video = res.body.data.find(v => v.name === videoAttributes.name)
-        const res2 = await getVideo(server.url, video.id)
-        const videoDetails: VideoDetails = res2.body
+        const video = data.find(v => v.name === attributes.name)
+        const videoDetails = await server.videos.get({ id: video.id })
 
         expect(videoDetails.files).to.have.lengthOf(4)
         expect(videoDetails.files[0].fps).to.be.above(58).and.below(62)
@@ -502,14 +489,16 @@ describe('Test video transcoding', function () {
         expect(videoDetails.files[2].fps).to.be.below(31)
         expect(videoDetails.files[3].fps).to.be.below(31)
 
-        for (const resolution of [ '240', '360', '480' ]) {
-          const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-' + resolution + '.mp4'))
+        for (const resolution of [ 240, 360, 480 ]) {
+          const file = videoDetails.files.find(f => f.resolution.id === resolution)
+          const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
           const fps = await getVideoFileFPS(path)
 
           expect(fps).to.be.below(31)
         }
 
-        const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-720.mp4'))
+        const file = videoDetails.files.find(f => f.resolution.id === 720)
+        const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
         const fps = await getVideoFileFPS(path)
 
         expect(fps).to.be.above(58).and.below(62)
@@ -528,29 +517,32 @@ describe('Test video transcoding', function () {
         expect(fps).to.be.equal(59)
       }
 
-      const videoAttributes = {
+      const attributes = {
         name: '59fps video',
         description: '59fps video',
         fixture: tempFixturePath
       }
 
-      await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
+      await servers[1].videos.upload({ attributes })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        const video = res.body.data.find(v => v.name === videoAttributes.name)
+        const { id } = data.find(v => v.name === attributes.name)
+        const video = await server.videos.get({ id })
 
         {
-          const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-240.mp4'))
+          const file = video.files.find(f => f.resolution.id === 240)
+          const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
           const fps = await getVideoFileFPS(path)
           expect(fps).to.be.equal(25)
         }
 
         {
-          const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-720.mp4'))
+          const file = video.files.find(f => f.resolution.id === 720)
+          const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
           const fps = await getVideoFileFPS(path)
           expect(fps).to.be.equal(59)
         }
@@ -559,6 +551,7 @@ describe('Test video transcoding', function () {
   })
 
   describe('Bitrate control', function () {
+
     it('Should respect maximum bitrate values', async function () {
       this.timeout(160_000)
 
@@ -571,30 +564,32 @@ describe('Test video transcoding', function () {
         expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS))
       }
 
-      const videoAttributes = {
+      const attributes = {
         name: 'high bitrate video',
         description: 'high bitrate video',
         fixture: tempFixturePath
       }
 
-      await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
+      await servers[1].videos.upload({ attributes })
 
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
+        const { data } = await server.videos.list()
 
-        const video = res.body.data.find(v => v.name === videoAttributes.name)
+        const { id } = data.find(v => v.name === attributes.name)
+        const video = await server.videos.get({ id })
 
-        for (const resolution of [ '240', '360', '480', '720', '1080' ]) {
-          const path = buildServerDirectory(servers[1], join('videos', video.uuid + '-' + resolution + '.mp4'))
+        for (const resolution of [ 240, 360, 480, 720, 1080 ]) {
+          const file = video.files.find(f => f.resolution.id === resolution)
+          const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
 
           const bitrate = await getVideoFileBitrate(path)
           const fps = await getVideoFileFPS(path)
-          const resolution2 = await getVideoFileResolution(path)
+          const { videoFileResolution } = await getVideoFileResolution(path)
 
-          expect(resolution2.videoFileResolution.toString()).to.equal(resolution)
-          expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS))
+          expect(videoFileResolution).to.equal(resolution)
+          expect(bitrate).to.be.below(getMaxBitrate(videoFileResolution, fps, VIDEO_TRANSCODING_FPS))
         }
       }
     })
@@ -602,7 +597,7 @@ describe('Test video transcoding', function () {
     it('Should not transcode to an higher bitrate than the original file', async function () {
       this.timeout(160_000)
 
-      const config = {
+      const newConfig = {
         transcoding: {
           enabled: true,
           resolutions: {
@@ -618,22 +613,25 @@ describe('Test video transcoding', function () {
           hls: { enabled: true }
         }
       }
-      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
+      await servers[1].config.updateCustomSubConfig({ newConfig })
 
-      const videoAttributes = {
+      const attributes = {
         name: 'low bitrate',
         fixture: 'low-bitrate.mp4'
       }
 
-      const resUpload = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
-      const videoUUID = resUpload.body.video.uuid
+      const { id } = await servers[1].videos.upload({ attributes })
 
       await waitJobs(servers)
 
+      const video = await servers[1].videos.get({ id })
+
       const resolutions = [ 240, 360, 480, 720, 1080 ]
       for (const r of resolutions) {
-        const path = `videos/${videoUUID}-${r}.mp4`
-        const size = await getServerFileSize(servers[1], path)
+        const file = video.files.find(f => f.resolution.id === r)
+
+        const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
+        const size = await getFileSize(path)
         expect(size, `${path} not below ${60_000}`).to.be.below(60_000)
       }
     })
@@ -644,11 +642,13 @@ describe('Test video transcoding', function () {
     it('Should provide valid ffprobe data', async function () {
       this.timeout(160_000)
 
-      const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'ffprobe data' })).uuid
+      const videoUUID = (await servers[1].videos.quickUpload({ name: 'ffprobe data' })).uuid
       await waitJobs(servers)
 
       {
-        const path = buildServerDirectory(servers[1], join('videos', videoUUID + '-240.mp4'))
+        const video = await servers[1].videos.get({ id: videoUUID })
+        const file = video.files.find(f => f.resolution.id === 240)
+        const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
         const metadata = await getMetadataFromFile(path)
 
         // expected format properties
@@ -678,8 +678,7 @@ describe('Test video transcoding', function () {
       }
 
       for (const server of servers) {
-        const res2 = await getVideo(server.url, videoUUID)
-        const videoDetails: VideoDetails = res2.body
+        const videoDetails = await server.videos.get({ id: videoUUID })
 
         const videoFiles = videoDetails.files
                                       .concat(videoDetails.streamingPlaylists[0].files)
@@ -691,8 +690,7 @@ describe('Test video transcoding', function () {
           expect(file.metadataUrl).to.contain(servers[1].url)
           expect(file.metadataUrl).to.contain(videoUUID)
 
-          const res3 = await getVideoFileMetadataUrl(file.metadataUrl)
-          const metadata: FfprobeData = res3.body
+          const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
           expect(metadata).to.have.nested.property('format.size')
         }
       }
@@ -709,17 +707,14 @@ describe('Test video transcoding', function () {
   describe('Transcoding job queue', function () {
 
     it('Should have the appropriate priorities for transcoding jobs', async function () {
-      const res = await getJobsListPaginationAndSort({
-        url: servers[1].url,
-        accessToken: servers[1].accessToken,
+      const body = await servers[1].jobs.getJobsList({
         start: 0,
         count: 100,
         sort: '-createdAt',
         jobType: 'video-transcoding'
       })
 
-      const jobs = res.body.data as Job[]
-
+      const jobs = body.data
       const transcodingJobs = jobs.filter(j => j.data.videoUUID === video4k)
 
       expect(transcodingJobs).to.have.lengthOf(14)
index 7428b82c57cc99e8e758026bff7b8d1100ba6ebd..2306807bf94232562e239ea6d5bcdfcba39be815 100644 (file)
@@ -1,25 +1,18 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import { expect } from 'chai'
 import {
   cleanupTests,
-  createUser,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
   makeGetRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo,
-  userLogin
-} from '../../../../shared/extra-utils'
-import { Video, VideoPrivacy } from '../../../../shared/models/videos'
-import { UserRole } from '../../../../shared/models/users'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-
-const expect = chai.expect
-
-async function getVideosNames (server: ServerInfo, token: string, filter: string, statusCodeExpected = HttpStatusCode.OK_200) {
+  PeerTubeServer,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode, UserRole, Video, VideoPrivacy } from '@shared/models'
+
+async function getVideosNames (server: PeerTubeServer, token: string, filter: string, expectedStatus = HttpStatusCode.OK_200) {
   const paths = [
     '/api/v1/video-channels/root_channel/videos',
     '/api/v1/accounts/root/videos',
@@ -38,7 +31,7 @@ async function getVideosNames (server: ServerInfo, token: string, filter: string
         sort: 'createdAt',
         filter
       },
-      statusCodeExpected
+      expectedStatus
     })
 
     videosResults.push(res.body.data.map(v => v.name))
@@ -48,42 +41,32 @@ async function getVideosNames (server: ServerInfo, token: string, filter: string
 }
 
 describe('Test videos filter', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
 
   // ---------------------------------------------------------------
 
   before(async function () {
     this.timeout(160000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
 
     for (const server of servers) {
       const moderator = { username: 'moderator', password: 'my super password' }
-      await createUser(
-        {
-          url: server.url,
-          accessToken: server.accessToken,
-          username: moderator.username,
-          password: moderator.password,
-          videoQuota: undefined,
-          videoQuotaDaily: undefined,
-          role: UserRole.MODERATOR
-        }
-      )
-      server['moderatorAccessToken'] = await userLogin(server, moderator)
+      await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
+      server['moderatorAccessToken'] = await server.login.getAccessToken(moderator)
 
-      await uploadVideo(server.url, server.accessToken, { name: 'public ' + server.serverNumber })
+      await server.videos.upload({ attributes: { name: 'public ' + server.serverNumber } })
 
       {
         const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
-        await uploadVideo(server.url, server.accessToken, attributes)
+        await server.videos.upload({ attributes })
       }
 
       {
         const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
-        await uploadVideo(server.url, server.accessToken, attributes)
+        await server.videos.upload({ attributes })
       }
     }
 
index b25cff879603fc97e60ee4468507521448a6a6ef..e4bc0bb3a3569f776dd033ecdd1e52bc276ba4b0 100644 (file)
@@ -1,74 +1,66 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import {
   cleanupTests,
-  createUser,
-  flushAndRunServer,
-  getVideosListWithToken,
-  getVideoWithToken,
+  createSingleServer,
+  HistoryCommand,
   killallServers,
-  reRunServer,
-  searchVideoWithToken,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updateMyUser,
-  uploadVideo,
-  userLogin,
   wait
-} from '../../../../shared/extra-utils'
-import { Video, VideoDetails } from '../../../../shared/models/videos'
-import { listMyVideosHistory, removeMyVideosHistory, userWatchVideo } from '../../../../shared/extra-utils/videos/video-history'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+} from '@shared/extra-utils'
+import { HttpStatusCode, Video } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test videos history', function () {
-  let server: ServerInfo = null
+  let server: PeerTubeServer = null
   let video1UUID: string
   let video2UUID: string
   let video3UUID: string
   let video3WatchedDate: Date
   let userAccessToken: string
+  let command: HistoryCommand
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
 
+    command = server.history
+
     {
-      const res = await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
-      video1UUID = res.body.video.uuid
+      const { uuid } = await server.videos.upload({ attributes: { name: 'video 1' } })
+      video1UUID = uuid
     }
 
     {
-      const res = await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
-      video2UUID = res.body.video.uuid
+      const { uuid } = await server.videos.upload({ attributes: { name: 'video 2' } })
+      video2UUID = uuid
     }
 
     {
-      const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
-      video3UUID = res.body.video.uuid
+      const { uuid } = await server.videos.upload({ attributes: { name: 'video 3' } })
+      video3UUID = uuid
     }
 
     const user = {
       username: 'user_1',
       password: 'super password'
     }
-    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
-    userAccessToken = await userLogin(server, user)
+    await server.users.create({ username: user.username, password: user.password })
+    userAccessToken = await server.login.getAccessToken(user)
   })
 
   it('Should get videos, without watching history', async function () {
-    const res = await getVideosListWithToken(server.url, server.accessToken)
-    const videos: Video[] = res.body.data
+    const { data } = await server.videos.listWithToken()
 
-    for (const video of videos) {
-      const resDetail = await getVideoWithToken(server.url, server.accessToken, video.id)
-      const videoDetails: VideoDetails = resDetail.body
+    for (const video of data) {
+      const videoDetails = await server.videos.getWithToken({ id: video.id })
 
       expect(video.userHistory).to.be.undefined
       expect(videoDetails.userHistory).to.be.undefined
@@ -76,21 +68,21 @@ describe('Test videos history', function () {
   })
 
   it('Should watch the first and second video', async function () {
-    await userWatchVideo(server.url, server.accessToken, video2UUID, 8)
-    await userWatchVideo(server.url, server.accessToken, video1UUID, 3)
+    await command.wathVideo({ videoId: video2UUID, currentTime: 8 })
+    await command.wathVideo({ videoId: video1UUID, currentTime: 3 })
   })
 
   it('Should return the correct history when listing, searching and getting videos', async function () {
     const videosOfVideos: Video[][] = []
 
     {
-      const res = await getVideosListWithToken(server.url, server.accessToken)
-      videosOfVideos.push(res.body.data)
+      const { data } = await server.videos.listWithToken()
+      videosOfVideos.push(data)
     }
 
     {
-      const res = await searchVideoWithToken(server.url, 'video', server.accessToken)
-      videosOfVideos.push(res.body.data)
+      const body = await server.search.searchVideos({ token: server.accessToken, search: 'video' })
+      videosOfVideos.push(body.data)
     }
 
     for (const videos of videosOfVideos) {
@@ -108,24 +100,21 @@ describe('Test videos history', function () {
     }
 
     {
-      const resDetail = await getVideoWithToken(server.url, server.accessToken, video1UUID)
-      const videoDetails: VideoDetails = resDetail.body
+      const videoDetails = await server.videos.getWithToken({ id: video1UUID })
 
       expect(videoDetails.userHistory).to.not.be.undefined
       expect(videoDetails.userHistory.currentTime).to.equal(3)
     }
 
     {
-      const resDetail = await getVideoWithToken(server.url, server.accessToken, video2UUID)
-      const videoDetails: VideoDetails = resDetail.body
+      const videoDetails = await server.videos.getWithToken({ id: video2UUID })
 
       expect(videoDetails.userHistory).to.not.be.undefined
       expect(videoDetails.userHistory.currentTime).to.equal(8)
     }
 
     {
-      const resDetail = await getVideoWithToken(server.url, server.accessToken, video3UUID)
-      const videoDetails: VideoDetails = resDetail.body
+      const videoDetails = await server.videos.getWithToken({ id: video3UUID })
 
       expect(videoDetails.userHistory).to.be.undefined
     }
@@ -133,71 +122,64 @@ describe('Test videos history', function () {
 
   it('Should have these videos when listing my history', async function () {
     video3WatchedDate = new Date()
-    await userWatchVideo(server.url, server.accessToken, video3UUID, 2)
+    await command.wathVideo({ videoId: video3UUID, currentTime: 2 })
 
-    const res = await listMyVideosHistory(server.url, server.accessToken)
+    const body = await command.list()
 
-    expect(res.body.total).to.equal(3)
+    expect(body.total).to.equal(3)
 
-    const videos: Video[] = res.body.data
+    const videos = body.data
     expect(videos[0].name).to.equal('video 3')
     expect(videos[1].name).to.equal('video 1')
     expect(videos[2].name).to.equal('video 2')
   })
 
   it('Should not have videos history on another user', async function () {
-    const res = await listMyVideosHistory(server.url, userAccessToken)
+    const body = await command.list({ token: userAccessToken })
 
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.have.lengthOf(0)
+    expect(body.total).to.equal(0)
+    expect(body.data).to.have.lengthOf(0)
   })
 
   it('Should be able to search through videos in my history', async function () {
-    const res = await listMyVideosHistory(server.url, server.accessToken, '2')
-
-    expect(res.body.total).to.equal(1)
+    const body = await command.list({ search: '2' })
+    expect(body.total).to.equal(1)
 
-    const videos: Video[] = res.body.data
+    const videos = body.data
     expect(videos[0].name).to.equal('video 2')
   })
 
   it('Should clear my history', async function () {
-    await removeMyVideosHistory(server.url, server.accessToken, video3WatchedDate.toISOString())
+    await command.remove({ beforeDate: video3WatchedDate.toISOString() })
   })
 
   it('Should have my history cleared', async function () {
-    const res = await listMyVideosHistory(server.url, server.accessToken)
+    const body = await command.list()
+    expect(body.total).to.equal(1)
 
-    expect(res.body.total).to.equal(1)
-
-    const videos: Video[] = res.body.data
+    const videos = body.data
     expect(videos[0].name).to.equal('video 3')
   })
 
   it('Should disable videos history', async function () {
-    await updateMyUser({
-      url: server.url,
-      accessToken: server.accessToken,
+    await server.users.updateMe({
       videosHistoryEnabled: false
     })
 
-    await userWatchVideo(server.url, server.accessToken, video2UUID, 8, HttpStatusCode.CONFLICT_409)
+    await command.wathVideo({ videoId: video2UUID, currentTime: 8, expectedStatus: HttpStatusCode.CONFLICT_409 })
   })
 
   it('Should re-enable videos history', async function () {
-    await updateMyUser({
-      url: server.url,
-      accessToken: server.accessToken,
+    await server.users.updateMe({
       videosHistoryEnabled: true
     })
 
-    await userWatchVideo(server.url, server.accessToken, video1UUID, 8)
-
-    const res = await listMyVideosHistory(server.url, server.accessToken)
+    await command.wathVideo({ videoId: video1UUID, currentTime: 8 })
 
-    expect(res.body.total).to.equal(2)
+    const body = await command.list()
+    expect(body.total).to.equal(2)
 
-    const videos: Video[] = res.body.data
+    const videos = body.data
     expect(videos[0].name).to.equal('video 1')
     expect(videos[1].name).to.equal('video 3')
   })
@@ -205,30 +187,29 @@ describe('Test videos history', function () {
   it('Should not clean old history', async function () {
     this.timeout(50000)
 
-    killallServers([ server ])
+    await killallServers([ server ])
 
-    await reRunServer(server, { history: { videos: { max_age: '10 days' } } })
+    await server.run({ history: { videos: { max_age: '10 days' } } })
 
     await wait(6000)
 
     // Should still have history
 
-    const res = await listMyVideosHistory(server.url, server.accessToken)
-
-    expect(res.body.total).to.equal(2)
+    const body = await command.list()
+    expect(body.total).to.equal(2)
   })
 
   it('Should clean old history', async function () {
     this.timeout(50000)
 
-    killallServers([ server ])
+    await killallServers([ server ])
 
-    await reRunServer(server, { history: { videos: { max_age: '5 seconds' } } })
+    await server.run({ history: { videos: { max_age: '5 seconds' } } })
 
     await wait(6000)
 
-    const res = await listMyVideosHistory(server.url, server.accessToken)
-    expect(res.body.total).to.equal(0)
+    const body = await command.list()
+    expect(body.total).to.equal(0)
   })
 
   after(async function () {
index c266a1dc55646c30af453ceb74b95ed798984a71..70aa665499c9ef7d50102e7ba27cb6728a34e140 100644 (file)
@@ -2,29 +2,15 @@
 
 import 'mocha'
 import * as chai from 'chai'
-
-import {
-  cleanupTests,
-  flushAndRunServer,
-  generateUserAccessToken,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo,
-  wait
-} from '../../../../shared/extra-utils'
-import { getVideosOverview, getVideosOverviewWithToken } from '../../../../shared/extra-utils/overviews/overviews'
-import { VideosOverview } from '../../../../shared/models/overviews'
-import { addAccountToAccountBlocklist } from '@shared/extra-utils/users/blocklist'
-import { Response } from 'superagent'
+import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers, wait } from '@shared/extra-utils'
+import { VideosOverview } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test a videos overview', function () {
-  let server: ServerInfo = null
-
-  function testOverviewCount (res: Response, expected: number) {
-    const overview: VideosOverview = res.body
+  let server: PeerTubeServer = null
 
+  function testOverviewCount (overview: VideosOverview, expected: number) {
     expect(overview.tags).to.have.lengthOf(expected)
     expect(overview.categories).to.have.lengthOf(expected)
     expect(overview.channels).to.have.lengthOf(expected)
@@ -33,15 +19,15 @@ describe('Test a videos overview', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
 
     await setAccessTokensToServers([ server ])
   })
 
   it('Should send empty overview', async function () {
-    const res = await getVideosOverview(server.url, 1)
+    const body = await server.overviews.getVideos({ page: 1 })
 
-    testOverviewCount(res, 0)
+    testOverviewCount(body, 0)
   })
 
   it('Should upload 5 videos in a specific category, tag and channel but not include them in overview', async function () {
@@ -49,40 +35,45 @@ describe('Test a videos overview', function () {
 
     await wait(3000)
 
-    await uploadVideo(server.url, server.accessToken, {
-      name: 'video 0',
-      category: 3,
-      tags: [ 'coucou1', 'coucou2' ]
+    await server.videos.upload({
+      attributes: {
+        name: 'video 0',
+        category: 3,
+        tags: [ 'coucou1', 'coucou2' ]
+      }
     })
 
-    const res = await getVideosOverview(server.url, 1)
+    const body = await server.overviews.getVideos({ page: 1 })
 
-    testOverviewCount(res, 0)
+    testOverviewCount(body, 0)
   })
 
   it('Should upload another video and include all videos in the overview', async function () {
     this.timeout(30000)
 
-    for (let i = 1; i < 6; i++) {
-      await uploadVideo(server.url, server.accessToken, {
-        name: 'video ' + i,
-        category: 3,
-        tags: [ 'coucou1', 'coucou2' ]
-      })
+    {
+      for (let i = 1; i < 6; i++) {
+        await server.videos.upload({
+          attributes: {
+            name: 'video ' + i,
+            category: 3,
+            tags: [ 'coucou1', 'coucou2' ]
+          }
+        })
+      }
+
+      await wait(3000)
     }
 
-    await wait(3000)
-
     {
-      const res = await getVideosOverview(server.url, 1)
+      const body = await server.overviews.getVideos({ page: 1 })
 
-      testOverviewCount(res, 1)
+      testOverviewCount(body, 1)
     }
 
     {
-      const res = await getVideosOverview(server.url, 2)
+      const overview = await server.overviews.getVideos({ page: 2 })
 
-      const overview: VideosOverview = res.body
       expect(overview.tags).to.have.lengthOf(1)
       expect(overview.categories).to.have.lengthOf(0)
       expect(overview.channels).to.have.lengthOf(0)
@@ -90,20 +81,10 @@ describe('Test a videos overview', function () {
   })
 
   it('Should have the correct overview', async function () {
-    const res1 = await getVideosOverview(server.url, 1)
-    const res2 = await getVideosOverview(server.url, 2)
-
-    const overview1: VideosOverview = res1.body
-    const overview2: VideosOverview = res2.body
-
-    const tmp = [
-      overview1.tags,
-      overview1.categories,
-      overview1.channels,
-      overview2.tags
-    ]
+    const overview1 = await server.overviews.getVideos({ page: 1 })
+    const overview2 = await server.overviews.getVideos({ page: 2 })
 
-    for (const arr of tmp) {
+    for (const arr of [ overview1.tags, overview1.categories, overview1.channels, overview2.tags ]) {
       expect(arr).to.have.lengthOf(1)
 
       const obj = arr[0]
@@ -127,20 +108,20 @@ describe('Test a videos overview', function () {
   })
 
   it('Should hide muted accounts', async function () {
-    const token = await generateUserAccessToken(server, 'choco')
+    const token = await server.users.generateUserAndToken('choco')
 
-    await addAccountToAccountBlocklist(server.url, token, 'root@' + server.host)
+    await server.blocklist.addToMyBlocklist({ token, account: 'root@' + server.host })
 
     {
-      const res = await getVideosOverview(server.url, 1)
+      const body = await server.overviews.getVideos({ page: 1 })
 
-      testOverviewCount(res, 1)
+      testOverviewCount(body, 1)
     }
 
     {
-      const res = await getVideosOverviewWithToken(server.url, 1, token)
+      const body = await server.overviews.getVideos({ page: 1, token })
 
-      testOverviewCount(res, 0)
+      testOverviewCount(body, 0)
     }
   })
 
index b89f332171e90e5a12ed9db4a39f73068ba2b758..82268b1bed019c074def3744df628ed8060b0857 100644 (file)
@@ -1,19 +1,14 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import {
   cleanupTests,
-  closeAllSequelize,
-  countVideoViewsOf,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
   killallServers,
-  reRunServer,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  uploadVideoAndGetId,
-  viewVideo,
   wait,
   waitJobs
 } from '../../../../shared/extra-utils'
@@ -21,7 +16,7 @@ import {
 const expect = chai.expect
 
 describe('Test video views cleaner', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
 
   let videoIdServer1: string
   let videoIdServer2: string
@@ -29,20 +24,20 @@ describe('Test video views cleaner', function () {
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
 
     await doubleFollow(servers[0], servers[1])
 
-    videoIdServer1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' })).uuid
-    videoIdServer2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' })).uuid
+    videoIdServer1 = (await servers[0].videos.quickUpload({ name: 'video server 1' })).uuid
+    videoIdServer2 = (await servers[1].videos.quickUpload({ name: 'video server 2' })).uuid
 
     await waitJobs(servers)
 
-    await viewVideo(servers[0].url, videoIdServer1)
-    await viewVideo(servers[1].url, videoIdServer1)
-    await viewVideo(servers[0].url, videoIdServer2)
-    await viewVideo(servers[1].url, videoIdServer2)
+    await servers[0].videos.view({ id: videoIdServer1 })
+    await servers[1].videos.view({ id: videoIdServer1 })
+    await servers[0].videos.view({ id: videoIdServer2 })
+    await servers[1].videos.view({ id: videoIdServer2 })
 
     await waitJobs(servers)
   })
@@ -50,9 +45,9 @@ describe('Test video views cleaner', function () {
   it('Should not clean old video views', async function () {
     this.timeout(50000)
 
-    killallServers([ servers[0] ])
+    await killallServers([ servers[0] ])
 
-    await reRunServer(servers[0], { views: { videos: { remote: { max_age: '10 days' } } } })
+    await servers[0].run({ views: { videos: { remote: { max_age: '10 days' } } } })
 
     await wait(6000)
 
@@ -60,14 +55,14 @@ describe('Test video views cleaner', function () {
 
     {
       for (const server of servers) {
-        const total = await countVideoViewsOf(server.internalServerNumber, videoIdServer1)
+        const total = await server.sql.countVideoViewsOf(videoIdServer1)
         expect(total).to.equal(2, 'Server ' + server.serverNumber + ' does not have the correct amount of views')
       }
     }
 
     {
       for (const server of servers) {
-        const total = await countVideoViewsOf(server.internalServerNumber, videoIdServer2)
+        const total = await server.sql.countVideoViewsOf(videoIdServer2)
         expect(total).to.equal(2, 'Server ' + server.serverNumber + ' does not have the correct amount of views')
       }
     }
@@ -76,9 +71,9 @@ describe('Test video views cleaner', function () {
   it('Should clean old video views', async function () {
     this.timeout(50000)
 
-    killallServers([ servers[0] ])
+    await killallServers([ servers[0] ])
 
-    await reRunServer(servers[0], { views: { videos: { remote: { max_age: '5 seconds' } } } })
+    await servers[0].run({ views: { videos: { remote: { max_age: '5 seconds' } } } })
 
     await wait(6000)
 
@@ -86,23 +81,21 @@ describe('Test video views cleaner', function () {
 
     {
       for (const server of servers) {
-        const total = await countVideoViewsOf(server.internalServerNumber, videoIdServer1)
+        const total = await server.sql.countVideoViewsOf(videoIdServer1)
         expect(total).to.equal(2)
       }
     }
 
     {
-      const totalServer1 = await countVideoViewsOf(servers[0].internalServerNumber, videoIdServer2)
+      const totalServer1 = await servers[0].sql.countVideoViewsOf(videoIdServer2)
       expect(totalServer1).to.equal(0)
 
-      const totalServer2 = await countVideoViewsOf(servers[1].internalServerNumber, videoIdServer2)
+      const totalServer2 = await servers[1].sql.countVideoViewsOf(videoIdServer2)
       expect(totalServer2).to.equal(2)
     }
   })
 
   after(async function () {
-    await closeAllSequelize(servers)
-
     await cleanupTests(servers)
   })
 })
index 49758ff56cf64872c89e1d8778b45205058864f7..bddcff5e70590ba3e1c549ad4fc2f948b3643e1f 100644 (file)
@@ -2,21 +2,8 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { VideoFile } from '@shared/models/videos/video-file.model'
-import {
-  cleanupTests,
-  doubleFollow,
-  execCLI,
-  flushAndRunMultipleServers,
-  getEnvCli,
-  getVideo,
-  getVideosList,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo
-} from '../../../shared/extra-utils'
-import { waitJobs } from '../../../shared/extra-utils/server/jobs'
-import { VideoDetails } from '../../../shared/models/videos'
+import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
+import { VideoFile } from '@shared/models'
 
 const expect = chai.expect
 
@@ -33,7 +20,7 @@ function assertVideoProperties (video: VideoFile, resolution: number, extname: s
 describe('Test create import video jobs', function () {
   this.timeout(60000)
 
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let video1UUID: string
   let video2UUID: string
 
@@ -41,56 +28,61 @@ describe('Test create import video jobs', function () {
     this.timeout(90000)
 
     // Run server 2 to have transcoding enabled
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
 
     await doubleFollow(servers[0], servers[1])
 
     // Upload two videos for our needs
-    const res1 = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video1' })
-    video1UUID = res1.body.video.uuid
-    const res2 = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' })
-    video2UUID = res2.body.video.uuid
+    {
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video1' } })
+      video1UUID = uuid
+    }
+
+    {
+      const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video2' } })
+      video2UUID = uuid
+    }
 
     // Transcoding
     await waitJobs(servers)
   })
 
   it('Should run a import job on video 1 with a lower resolution', async function () {
-    const env = getEnvCli(servers[0])
-    await execCLI(`${env} npm run create-import-video-file-job -- -v ${video1UUID} -i server/tests/fixtures/video_short-480.webm`)
+    const command = `npm run create-import-video-file-job -- -v ${video1UUID} -i server/tests/fixtures/video_short-480.webm`
+    await servers[0].cli.execWithEnv(command)
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const { data: videos } = (await getVideosList(server.url)).body
+      const { data: videos } = await server.videos.list()
       expect(videos).to.have.lengthOf(2)
 
       const video = videos.find(({ uuid }) => uuid === video1UUID)
-      const videoDetail: VideoDetails = (await getVideo(server.url, video.uuid)).body
+      const videoDetails = await server.videos.get({ id: video.uuid })
 
-      expect(videoDetail.files).to.have.lengthOf(2)
-      const [ originalVideo, transcodedVideo ] = videoDetail.files
+      expect(videoDetails.files).to.have.lengthOf(2)
+      const [ originalVideo, transcodedVideo ] = videoDetails.files
       assertVideoProperties(originalVideo, 720, 'webm', 218910)
       assertVideoProperties(transcodedVideo, 480, 'webm', 69217)
     }
   })
 
   it('Should run a import job on video 2 with the same resolution and a different extension', async function () {
-    const env = getEnvCli(servers[1])
-    await execCLI(`${env} npm run create-import-video-file-job -- -v ${video2UUID} -i server/tests/fixtures/video_short.ogv`)
+    const command = `npm run create-import-video-file-job -- -v ${video2UUID} -i server/tests/fixtures/video_short.ogv`
+    await servers[1].cli.execWithEnv(command)
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const { data: videos } = (await getVideosList(server.url)).body
+      const { data: videos } = await server.videos.list()
       expect(videos).to.have.lengthOf(2)
 
       const video = videos.find(({ uuid }) => uuid === video2UUID)
-      const videoDetail: VideoDetails = (await getVideo(server.url, video.uuid)).body
+      const videoDetails = await server.videos.get({ id: video.uuid })
 
-      expect(videoDetail.files).to.have.lengthOf(4)
-      const [ originalVideo, transcodedVideo420, transcodedVideo320, transcodedVideo240 ] = videoDetail.files
+      expect(videoDetails.files).to.have.lengthOf(4)
+      const [ originalVideo, transcodedVideo420, transcodedVideo320, transcodedVideo240 ] = videoDetails.files
       assertVideoProperties(originalVideo, 720, 'ogv', 140849)
       assertVideoProperties(transcodedVideo420, 480, 'mp4')
       assertVideoProperties(transcodedVideo320, 360, 'mp4')
@@ -99,20 +91,20 @@ describe('Test create import video jobs', function () {
   })
 
   it('Should run a import job on video 2 with the same resolution and the same extension', async function () {
-    const env = getEnvCli(servers[0])
-    await execCLI(`${env} npm run create-import-video-file-job -- -v ${video1UUID} -i server/tests/fixtures/video_short2.webm`)
+    const command = `npm run create-import-video-file-job -- -v ${video1UUID} -i server/tests/fixtures/video_short2.webm`
+    await servers[0].cli.execWithEnv(command)
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const { data: videos } = (await getVideosList(server.url)).body
+      const { data: videos } = await server.videos.list()
       expect(videos).to.have.lengthOf(2)
 
       const video = videos.find(({ uuid }) => uuid === video1UUID)
-      const videoDetail: VideoDetails = (await getVideo(server.url, video.uuid)).body
+      const videoDetails = await server.videos.get({ id: video.uuid })
 
-      expect(videoDetail.files).to.have.lengthOf(2)
-      const [ video720, video480 ] = videoDetail.files
+      expect(videoDetails.files).to.have.lengthOf(2)
+      const [ video720, video480 ] = videoDetails.files
       assertVideoProperties(video720, 720, 'webm', 942961)
       assertVideoProperties(video480, 480, 'webm', 69217)
     }
index 5bc1687cd5621503a9aaae0b5317bb9b8f21c8b1..df787ccdcab7e9e072c001c201d093c1654654da 100644 (file)
@@ -2,26 +2,19 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { VideoDetails } from '../../../shared/models/videos'
 import {
   cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  execCLI,
-  flushAndRunMultipleServers,
-  getEnvCli,
-  getVideo,
-  getVideosList,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updateCustomSubConfig,
-  uploadVideo
+  waitJobs
 } from '../../../shared/extra-utils'
-import { waitJobs } from '../../../shared/extra-utils/server/jobs'
 
 const expect = chai.expect
 
 describe('Test create transcoding jobs', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   const videosUUID: string[] = []
 
   const config = {
@@ -46,16 +39,16 @@ describe('Test create transcoding jobs', function () {
     this.timeout(60000)
 
     // Run server 2 to have transcoding enabled
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
 
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+    await servers[0].config.updateCustomSubConfig({ newConfig: config })
 
     await doubleFollow(servers[0], servers[1])
 
     for (let i = 1; i <= 5; i++) {
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' + i })
-      videosUUID.push(res.body.video.uuid)
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video' + i } })
+      videosUUID.push(uuid)
     }
 
     await waitJobs(servers)
@@ -65,13 +58,11 @@ describe('Test create transcoding jobs', function () {
     this.timeout(30000)
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-      const videos = res.body.data
-      expect(videos).to.have.lengthOf(videosUUID.length)
+      const { data } = await server.videos.list()
+      expect(data).to.have.lengthOf(videosUUID.length)
 
-      for (const video of videos) {
-        const res2 = await getVideo(server.url, video.uuid)
-        const videoDetail: VideoDetails = res2.body
+      for (const video of data) {
+        const videoDetail = await server.videos.get({ id: video.uuid })
         expect(videoDetail.files).to.have.lengthOf(1)
         expect(videoDetail.streamingPlaylists).to.have.lengthOf(0)
       }
@@ -81,20 +72,16 @@ describe('Test create transcoding jobs', function () {
   it('Should run a transcoding job on video 2', async function () {
     this.timeout(60000)
 
-    const env = getEnvCli(servers[0])
-    await execCLI(`${env} npm run create-transcoding-job -- -v ${videosUUID[1]}`)
-
+    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[1]}`)
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-      const videos = res.body.data
+      const { data } = await server.videos.list()
 
       let infoHashes: { [id: number]: string }
 
-      for (const video of videos) {
-        const res2 = await getVideo(server.url, video.uuid)
-        const videoDetail: VideoDetails = res2.body
+      for (const video of data) {
+        const videoDetail = await server.videos.get({ id: video.uuid })
 
         if (video.uuid === videosUUID[1]) {
           expect(videoDetail.files).to.have.lengthOf(4)
@@ -123,43 +110,38 @@ describe('Test create transcoding jobs', function () {
   it('Should run a transcoding job on video 1 with resolution', async function () {
     this.timeout(60000)
 
-    const env = getEnvCli(servers[0])
-    await execCLI(`${env} npm run create-transcoding-job -- -v ${videosUUID[0]} -r 480`)
+    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[0]} -r 480`)
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-      const videos = res.body.data
-      expect(videos).to.have.lengthOf(videosUUID.length)
+      const { data } = await server.videos.list()
+      expect(data).to.have.lengthOf(videosUUID.length)
 
-      const res2 = await getVideo(server.url, videosUUID[0])
-      const videoDetail: VideoDetails = res2.body
+      const videoDetails = await server.videos.get({ id: videosUUID[0] })
 
-      expect(videoDetail.files).to.have.lengthOf(2)
-      expect(videoDetail.files[0].resolution.id).to.equal(720)
-      expect(videoDetail.files[1].resolution.id).to.equal(480)
+      expect(videoDetails.files).to.have.lengthOf(2)
+      expect(videoDetails.files[0].resolution.id).to.equal(720)
+      expect(videoDetails.files[1].resolution.id).to.equal(480)
 
-      expect(videoDetail.streamingPlaylists).to.have.lengthOf(0)
+      expect(videoDetails.streamingPlaylists).to.have.lengthOf(0)
     }
   })
 
   it('Should generate an HLS resolution', async function () {
     this.timeout(120000)
 
-    const env = getEnvCli(servers[0])
-    await execCLI(`${env} npm run create-transcoding-job -- -v ${videosUUID[2]} --generate-hls -r 480`)
+    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[2]} --generate-hls -r 480`)
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideo(server.url, videosUUID[2])
-      const videoDetail: VideoDetails = res.body
+      const videoDetails = await server.videos.get({ id: videosUUID[2] })
 
-      expect(videoDetail.files).to.have.lengthOf(1)
-      expect(videoDetail.streamingPlaylists).to.have.lengthOf(1)
+      expect(videoDetails.files).to.have.lengthOf(1)
+      expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
 
-      const files = videoDetail.streamingPlaylists[0].files
+      const files = videoDetails.streamingPlaylists[0].files
       expect(files).to.have.lengthOf(1)
       expect(files[0].resolution.id).to.equal(480)
     }
@@ -168,16 +150,14 @@ describe('Test create transcoding jobs', function () {
   it('Should not duplicate an HLS resolution', async function () {
     this.timeout(120000)
 
-    const env = getEnvCli(servers[0])
-    await execCLI(`${env} npm run create-transcoding-job -- -v ${videosUUID[2]} --generate-hls -r 480`)
+    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[2]} --generate-hls -r 480`)
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideo(server.url, videosUUID[2])
-      const videoDetail: VideoDetails = res.body
+      const videoDetails = await server.videos.get({ id: videosUUID[2] })
 
-      const files = videoDetail.streamingPlaylists[0].files
+      const files = videoDetails.streamingPlaylists[0].files
       expect(files).to.have.lengthOf(1)
       expect(files[0].resolution.id).to.equal(480)
     }
@@ -186,19 +166,17 @@ describe('Test create transcoding jobs', function () {
   it('Should generate all HLS resolutions', async function () {
     this.timeout(120000)
 
-    const env = getEnvCli(servers[0])
-    await execCLI(`${env} npm run create-transcoding-job -- -v ${videosUUID[3]} --generate-hls`)
+    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[3]} --generate-hls`)
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideo(server.url, videosUUID[3])
-      const videoDetail: VideoDetails = res.body
+      const videoDetails = await server.videos.get({ id: videosUUID[3] })
 
-      expect(videoDetail.files).to.have.lengthOf(1)
-      expect(videoDetail.streamingPlaylists).to.have.lengthOf(1)
+      expect(videoDetails.files).to.have.lengthOf(1)
+      expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
 
-      const files = videoDetail.streamingPlaylists[0].files
+      const files = videoDetails.streamingPlaylists[0].files
       expect(files).to.have.lengthOf(4)
     }
   })
@@ -207,20 +185,18 @@ describe('Test create transcoding jobs', function () {
     this.timeout(120000)
 
     config.transcoding.hls.enabled = true
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+    await servers[0].config.updateCustomSubConfig({ newConfig: config })
 
-    const env = getEnvCli(servers[0])
-    await execCLI(`${env} npm run create-transcoding-job -- -v ${videosUUID[4]}`)
+    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[4]}`)
 
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideo(server.url, videosUUID[4])
-      const videoDetail: VideoDetails = res.body
+      const videoDetails = await server.videos.get({ id: videosUUID[4] })
 
-      expect(videoDetail.files).to.have.lengthOf(4)
-      expect(videoDetail.streamingPlaylists).to.have.lengthOf(1)
-      expect(videoDetail.streamingPlaylists[0].files).to.have.lengthOf(4)
+      expect(videoDetails.files).to.have.lengthOf(4)
+      expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
+      expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(4)
     }
   })
 
index 91a1c9cc4a789d6baed08776e7a273438657eeaf..579b2e7d8955cacb519472ae842e884181601983 100644 (file)
@@ -2,38 +2,30 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { join } from 'path'
 import {
-  buildServerDirectory,
   cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  execCLI,
-  flushAndRunMultipleServers,
   generateHighBitrateVideo,
-  getEnvCli,
-  getVideo,
-  getVideosList,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  uploadVideo,
-  viewVideo,
-  wait
-} from '../../../shared/extra-utils'
-import { waitJobs } from '../../../shared/extra-utils/server/jobs'
-import { getMaxBitrate, Video, VideoDetails, VideoResolution } from '../../../shared/models/videos'
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { getMaxBitrate, VideoResolution } from '@shared/models'
 import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffprobe-utils'
 import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
 
 const expect = chai.expect
 
 describe('Test optimize old videos', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
 
   before(async function () {
     this.timeout(200000)
 
     // Run server 2 to have transcoding enabled
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
 
     await doubleFollow(servers[0], servers[1])
@@ -48,8 +40,8 @@ describe('Test optimize old videos', function () {
     }
 
     // Upload two videos for our needs
-    await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video1', fixture: tempFixturePath })
-    await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video2', fixture: tempFixturePath })
+    await servers[0].videos.upload({ attributes: { name: 'video1', fixture: tempFixturePath } })
+    await servers[0].videos.upload({ attributes: { name: 'video2', fixture: tempFixturePath } })
 
     await waitJobs(servers)
   })
@@ -58,14 +50,12 @@ describe('Test optimize old videos', function () {
     this.timeout(30000)
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-      const videos = res.body.data
-      expect(videos).to.have.lengthOf(2)
-
-      for (const video of videos) {
-        const res2 = await getVideo(server.url, video.uuid)
-        const videoDetail: VideoDetails = res2.body
-        expect(videoDetail.files).to.have.lengthOf(1)
+      const { data } = await server.videos.list()
+      expect(data).to.have.lengthOf(2)
+
+      for (const video of data) {
+        const videoDetails = await server.videos.get({ id: video.uuid })
+        expect(videoDetails.files).to.have.lengthOf(1)
       }
     }
   })
@@ -73,34 +63,29 @@ describe('Test optimize old videos', function () {
   it('Should run optimize script', async function () {
     this.timeout(200000)
 
-    const env = getEnvCli(servers[0])
-    await execCLI(`${env} npm run optimize-old-videos`)
-
+    await servers[0].cli.execWithEnv('npm run optimize-old-videos')
     await waitJobs(servers)
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-      const videos: Video[] = res.body.data
-
-      expect(videos).to.have.lengthOf(2)
+      const { data } = await server.videos.list()
+      expect(data).to.have.lengthOf(2)
 
-      for (const video of videos) {
-        await viewVideo(server.url, video.uuid)
+      for (const video of data) {
+        await server.videos.view({ id: video.uuid })
 
         // Refresh video
         await waitJobs(servers)
         await wait(5000)
         await waitJobs(servers)
 
-        const res2 = await getVideo(server.url, video.uuid)
-        const videosDetails: VideoDetails = res2.body
+        const videoDetails = await server.videos.get({ id: video.uuid })
 
-        expect(videosDetails.files).to.have.lengthOf(1)
-        const file = videosDetails.files[0]
+        expect(videoDetails.files).to.have.lengthOf(1)
+        const file = videoDetails.files[0]
 
         expect(file.size).to.be.below(8000000)
 
-        const path = buildServerDirectory(servers[0], join('videos', video.uuid + '-' + file.resolution.id + '.mp4'))
+        const path = servers[0].servers.buildWebTorrentFilePath(file.fileUrl)
         const bitrate = await getVideoFileBitrate(path)
         const fps = await getVideoFileFPS(path)
         const resolution = await getVideoFileResolution(path)
index fcf7e2e2e50c253b14d187a7f82f0bc490612fb5..f2a9849628547a11b853a35b05e13c96297a9f68 100644 (file)
@@ -2,97 +2,91 @@
 
 import 'mocha'
 import { expect } from 'chai'
-import { Video, VideoDetails } from '../../../shared'
 import {
-  addVideoChannel,
   areHttpImportTestsDisabled,
   buildAbsoluteFixturePath,
   cleanupTests,
-  createUser,
+  CLICommand,
+  createSingleServer,
   doubleFollow,
-  execCLI,
-  flushAndRunServer,
-  getEnvCli,
-  getLocalIdByUUID,
-  getVideo,
-  getVideosList,
-  removeVideo,
-  ServerInfo,
+  FIXTURE_URLS,
+  PeerTubeServer,
   setAccessTokensToServers,
   testHelloWorldRegisteredSettings,
-  uploadVideoAndGetId,
-  userLogin,
   waitJobs
 } from '../../../shared/extra-utils'
-import { getYoutubeVideoUrl } from '../../../shared/extra-utils/videos/video-imports'
 
 describe('Test CLI wrapper', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let userAccessToken: string
 
+  let cliCommand: CLICommand
+
   const cmd = 'node ./dist/server/tools/peertube.js'
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
-    await createUser({ url: server.url, accessToken: server.accessToken, username: 'user_1', password: 'super_password' })
+    await server.users.create({ username: 'user_1', password: 'super_password' })
 
-    userAccessToken = await userLogin(server, { username: 'user_1', password: 'super_password' })
+    userAccessToken = await server.login.getAccessToken({ username: 'user_1', password: 'super_password' })
 
     {
-      const args = { name: 'user_channel', displayName: 'User channel', support: 'super support text' }
-      await addVideoChannel(server.url, userAccessToken, args)
+      const attributes = { name: 'user_channel', displayName: 'User channel', support: 'super support text' }
+      await server.channels.create({ token: userAccessToken, attributes })
     }
+
+    cliCommand = server.cli
   })
 
   describe('Authentication and instance selection', function () {
 
+    it('Should get an access token', async function () {
+      const stdout = await cliCommand.execWithEnv(`${cmd} token --url ${server.url} --username user_1 --password super_password`)
+      const token = stdout.trim()
+
+      const body = await server.users.getMyInfo({ token })
+      expect(body.username).to.equal('user_1')
+    })
+
     it('Should display no selected instance', async function () {
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-      const stdout = await execCLI(`${env} ${cmd} --help`)
-
+      const stdout = await cliCommand.execWithEnv(`${cmd} --help`)
       expect(stdout).to.contain('no instance selected')
     })
 
     it('Should add a user', async function () {
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-      await execCLI(`${env} ${cmd} auth add -u ${server.url} -U user_1 -p super_password`)
+      await cliCommand.execWithEnv(`${cmd} auth add -u ${server.url} -U user_1 -p super_password`)
     })
 
     it('Should not fail to add a user if there is a slash at the end of the instance URL', async function () {
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-      let fullServerURL
-      fullServerURL = server.url + '/'
-      await execCLI(`${env} ${cmd} auth add -u ${fullServerURL} -U user_1 -p super_password`)
+      let fullServerURL = server.url + '/'
+
+      await cliCommand.execWithEnv(`${cmd} auth add -u ${fullServerURL} -U user_1 -p super_password`)
 
       fullServerURL = server.url + '/asdfasdf'
-      await execCLI(`${env} ${cmd} auth add -u ${fullServerURL} -U user_1 -p super_password`)
+      await cliCommand.execWithEnv(`${cmd} auth add -u ${fullServerURL} -U user_1 -p super_password`)
     })
 
     it('Should default to this user', async function () {
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-      const stdout = await execCLI(`${env} ${cmd} --help`)
-
+      const stdout = await cliCommand.execWithEnv(`${cmd} --help`)
       expect(stdout).to.contain(`instance ${server.url} selected`)
     })
 
     it('Should remember the user', async function () {
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-      const stdout = await execCLI(`${env} ${cmd} auth list`)
-
+      const stdout = await cliCommand.execWithEnv(`${cmd} auth list`)
       expect(stdout).to.contain(server.url)
     })
   })
@@ -102,24 +96,17 @@ describe('Test CLI wrapper', function () {
     it('Should upload a video', async function () {
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-
       const fixture = buildAbsoluteFixturePath('60fps_720p_small.mp4')
-
       const params = `-f ${fixture} --video-name 'test upload' --channel-name user_channel --support 'support_text'`
 
-      await execCLI(`${env} ${cmd} upload ${params}`)
+      await cliCommand.execWithEnv(`${cmd} upload ${params}`)
     })
 
     it('Should have the video uploaded', async function () {
-      const res = await getVideosList(server.url)
-
-      expect(res.body.total).to.equal(1)
-
-      const videos: Video[] = res.body.data
-
-      const video: VideoDetails = (await getVideo(server.url, videos[0].uuid)).body
+      const { total, data } = await server.videos.list()
+      expect(total).to.equal(1)
 
+      const video = await server.videos.get({ id: data[0].uuid })
       expect(video.name).to.equal('test upload')
       expect(video.support).to.equal('support_text')
       expect(video.channel.name).to.equal('user_channel')
@@ -130,11 +117,8 @@ describe('Test CLI wrapper', function () {
 
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-
-      const params = `--target-url ${getYoutubeVideoUrl()} --channel-name user_channel`
-
-      await execCLI(`${env} ${cmd} import ${params}`)
+      const params = `--target-url ${FIXTURE_URLS.youtube} --channel-name user_channel`
+      await cliCommand.execWithEnv(`${cmd} import ${params}`)
     })
 
     it('Should have imported the video', async function () {
@@ -144,21 +128,19 @@ describe('Test CLI wrapper', function () {
 
       await waitJobs([ server ])
 
-      const res = await getVideosList(server.url)
-
-      expect(res.body.total).to.equal(2)
+      const { total, data } = await server.videos.list()
+      expect(total).to.equal(2)
 
-      const videos: Video[] = res.body.data
-      const video = videos.find(v => v.name === 'small video - youtube')
+      const video = data.find(v => v.name === 'small video - youtube')
       expect(video).to.not.be.undefined
 
-      const videoDetails: VideoDetails = (await getVideo(server.url, video.id)).body
+      const videoDetails = await server.videos.get({ id: video.id })
       expect(videoDetails.channel.name).to.equal('user_channel')
       expect(videoDetails.support).to.equal('super support text')
       expect(videoDetails.nsfw).to.be.false
 
       // So we can reimport it
-      await removeVideo(server.url, userAccessToken, video.id)
+      await server.videos.remove({ token: userAccessToken, id: video.id })
     })
 
     it('Should import and override some imported attributes', async function () {
@@ -166,23 +148,20 @@ describe('Test CLI wrapper', function () {
 
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-
-      const params = `--target-url ${getYoutubeVideoUrl()} --channel-name user_channel --video-name toto --nsfw --support support`
-
-      await execCLI(`${env} ${cmd} import ${params}`)
+      const params = `--target-url ${FIXTURE_URLS.youtube} ` +
+                     `--channel-name user_channel --video-name toto --nsfw --support support`
+      await cliCommand.execWithEnv(`${cmd} import ${params}`)
 
       await waitJobs([ server ])
 
       {
-        const res = await getVideosList(server.url)
-        expect(res.body.total).to.equal(2)
+        const { total, data } = await server.videos.list()
+        expect(total).to.equal(2)
 
-        const videos: Video[] = res.body.data
-        const video = videos.find(v => v.name === 'toto')
+        const video = data.find(v => v.name === 'toto')
         expect(video).to.not.be.undefined
 
-        const videoDetails: VideoDetails = (await getVideo(server.url, video.id)).body
+        const videoDetails = await server.videos.get({ id: video.id })
         expect(videoDetails.channel.name).to.equal('user_channel')
         expect(videoDetails.support).to.equal('support')
         expect(videoDetails.nsfw).to.be.true
@@ -194,18 +173,14 @@ describe('Test CLI wrapper', function () {
   describe('Admin auth', function () {
 
     it('Should remove the auth user', async function () {
-      const env = getEnvCli(server)
-
-      await execCLI(`${env} ${cmd} auth del ${server.url}`)
-
-      const stdout = await execCLI(`${env} ${cmd} --help`)
+      await cliCommand.execWithEnv(`${cmd} auth del ${server.url}`)
 
+      const stdout = await cliCommand.execWithEnv(`${cmd} --help`)
       expect(stdout).to.contain('no instance selected')
     })
 
     it('Should add the admin user', async function () {
-      const env = getEnvCli(server)
-      await execCLI(`${env} ${cmd} auth add -u ${server.url} -U root -p test${server.internalServerNumber}`)
+      await cliCommand.execWithEnv(`${cmd} auth add -u ${server.url} -U root -p test${server.internalServerNumber}`)
     })
   })
 
@@ -214,8 +189,7 @@ describe('Test CLI wrapper', function () {
     it('Should install a plugin', async function () {
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-      await execCLI(`${env} ${cmd} plugins install --npm-name peertube-plugin-hello-world`)
+      await cliCommand.execWithEnv(`${cmd} plugins install --npm-name peertube-plugin-hello-world`)
     })
 
     it('Should have registered settings', async function () {
@@ -223,29 +197,27 @@ describe('Test CLI wrapper', function () {
     })
 
     it('Should list installed plugins', async function () {
-      const env = getEnvCli(server)
-      const res = await execCLI(`${env} ${cmd} plugins list`)
+      const res = await cliCommand.execWithEnv(`${cmd} plugins list`)
 
       expect(res).to.contain('peertube-plugin-hello-world')
     })
 
     it('Should uninstall the plugin', async function () {
-      const env = getEnvCli(server)
-      const res = await execCLI(`${env} ${cmd} plugins uninstall --npm-name peertube-plugin-hello-world`)
+      const res = await cliCommand.execWithEnv(`${cmd} plugins uninstall --npm-name peertube-plugin-hello-world`)
 
       expect(res).to.not.contain('peertube-plugin-hello-world')
     })
   })
 
   describe('Manage video redundancies', function () {
-    let anotherServer: ServerInfo
+    let anotherServer: PeerTubeServer
     let video1Server2: number
-    let servers: ServerInfo[]
+    let servers: PeerTubeServer[]
 
     before(async function () {
       this.timeout(120000)
 
-      anotherServer = await flushAndRunServer(2)
+      anotherServer = await createSingleServer(2)
       await setAccessTokensToServers([ anotherServer ])
 
       await doubleFollow(server, anotherServer)
@@ -253,20 +225,17 @@ describe('Test CLI wrapper', function () {
       servers = [ server, anotherServer ]
       await waitJobs(servers)
 
-      const uuid = (await uploadVideoAndGetId({ server: anotherServer, videoName: 'super video' })).uuid
+      const { uuid } = await anotherServer.videos.quickUpload({ name: 'super video' })
       await waitJobs(servers)
 
-      video1Server2 = await getLocalIdByUUID(server.url, uuid)
+      video1Server2 = await server.videos.getId({ uuid })
     })
 
     it('Should add a redundancy', async function () {
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-
       const params = `add --video ${video1Server2}`
-
-      await execCLI(`${env} ${cmd} redundancy ${params}`)
+      await cliCommand.execWithEnv(`${cmd} redundancy ${params}`)
 
       await waitJobs(servers)
     })
@@ -275,10 +244,8 @@ describe('Test CLI wrapper', function () {
       this.timeout(60000)
 
       {
-        const env = getEnvCli(server)
-
         const params = 'list-my-redundancies'
-        const stdout = await execCLI(`${env} ${cmd} redundancy ${params}`)
+        const stdout = await cliCommand.execWithEnv(`${cmd} redundancy ${params}`)
 
         expect(stdout).to.contain('super video')
         expect(stdout).to.contain(`localhost:${server.port}`)
@@ -288,18 +255,14 @@ describe('Test CLI wrapper', function () {
     it('Should remove a redundancy', async function () {
       this.timeout(60000)
 
-      const env = getEnvCli(server)
-
       const params = `remove --video ${video1Server2}`
-
-      await execCLI(`${env} ${cmd} redundancy ${params}`)
+      await cliCommand.execWithEnv(`${cmd} redundancy ${params}`)
 
       await waitJobs(servers)
 
       {
-        const env = getEnvCli(server)
         const params = 'list-my-redundancies'
-        const stdout = await execCLI(`${env} ${cmd} redundancy ${params}`)
+        const stdout = await cliCommand.execWithEnv(`${cmd} redundancy ${params}`)
 
         expect(stdout).to.not.contain('super video')
       }
index 7f19f14b7f7e6eac7c30033d4fa4aca8e748f0bf..07c78cc89ea9956b8246b362732efe99ff4f94d0 100644 (file)
@@ -1,55 +1,47 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
+import { expect } from 'chai'
 import {
   cleanupTests,
-  execCLI,
-  flushAndRunServer,
-  getConfig,
-  getEnvCli,
-  getPluginTestPath,
+  createSingleServer,
   killallServers,
-  reRunServer,
-  ServerInfo,
+  PeerTubeServer,
+  PluginsCommand,
   setAccessTokensToServers
 } from '../../../shared/extra-utils'
-import { ServerConfig } from '../../../shared/models/server'
-import { expect } from 'chai'
 
 describe('Test plugin scripts', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
   })
 
   it('Should install a plugin from stateless CLI', async function () {
     this.timeout(60000)
 
-    const packagePath = getPluginTestPath()
+    const packagePath = PluginsCommand.getPluginTestPath()
 
-    const env = getEnvCli(server)
-    await execCLI(`${env} npm run plugin:install -- --plugin-path ${packagePath}`)
+    await server.cli.execWithEnv(`npm run plugin:install -- --plugin-path ${packagePath}`)
   })
 
   it('Should install a theme from stateless CLI', async function () {
     this.timeout(60000)
 
-    const env = getEnvCli(server)
-    await execCLI(`${env} npm run plugin:install -- --npm-name peertube-theme-background-red`)
+    await server.cli.execWithEnv(`npm run plugin:install -- --npm-name peertube-theme-background-red`)
   })
 
   it('Should have the theme and the plugin registered when we restart peertube', async function () {
     this.timeout(30000)
 
-    killallServers([ server ])
-    await reRunServer(server)
+    await killallServers([ server ])
+    await server.run()
 
-    const res = await getConfig(server.url)
-    const config: ServerConfig = res.body
+    const config = await server.config.getConfig()
 
     const plugin = config.plugin.registered
                          .find(p => p.name === 'test')
@@ -63,18 +55,16 @@ describe('Test plugin scripts', function () {
   it('Should uninstall a plugin from stateless CLI', async function () {
     this.timeout(60000)
 
-    const env = getEnvCli(server)
-    await execCLI(`${env} npm run plugin:uninstall -- --npm-name peertube-plugin-test`)
+    await server.cli.execWithEnv(`npm run plugin:uninstall -- --npm-name peertube-plugin-test`)
   })
 
   it('Should have removed the plugin on another peertube restart', async function () {
     this.timeout(30000)
 
-    killallServers([ server ])
-    await reRunServer(server)
+    await killallServers([ server ])
+    await server.run()
 
-    const res = await getConfig(server.url)
-    const config: ServerConfig = res.body
+    const config = await server.config.getConfig()
 
     const plugin = config.plugin.registered
                          .find(p => p.name === 'test')
index 2d7255db7a281cc4b47ca58cae029d91e96bd224..3a7969e681884e4ef8cd7ae018b16495797c48c1 100644 (file)
@@ -2,14 +2,15 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { execCLI } from '../../../shared/extra-utils'
+import { getVideoFileBitrate, getVideoFileFPS } from '@server/helpers/ffprobe-utils'
+import { CLICommand } from '@shared/extra-utils'
 import { getTargetBitrate, VideoResolution } from '../../../shared/models/videos'
 import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
-import { getVideoFileBitrate, getVideoFileFPS } from '@server/helpers/ffprobe-utils'
 
 const expect = chai.expect
 
 describe('Test create transcoding jobs', function () {
+
   it('Should print the correct command for each resolution', async function () {
     const fixturePath = 'server/tests/fixtures/video_short.webm'
     const fps = await getVideoFileFPS(fixturePath)
@@ -19,7 +20,7 @@ describe('Test create transcoding jobs', function () {
       VideoResolution.H_720P,
       VideoResolution.H_1080P
     ]) {
-      const command = await execCLI(`npm run print-transcode-command -- ${fixturePath} -r ${resolution}`)
+      const command = await CLICommand.exec(`npm run print-transcode-command -- ${fixturePath} -r ${resolution}`)
       const targetBitrate = Math.min(getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS), bitrate)
 
       expect(command).to.includes(`-vf scale=w=-2:h=${resolution}`)
index a0af09de8d7788ae774bb8b0a4de6f4d5ec82b3c..2d4c02da74482aee2148e8a6f10f9591d00ccc2b 100644 (file)
@@ -5,87 +5,89 @@ import * as chai from 'chai'
 import { createFile, readdir } from 'fs-extra'
 import { join } from 'path'
 import { buildUUID } from '@server/helpers/uuid'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 import {
-  buildServerDirectory,
   cleanupTests,
-  createVideoPlaylist,
+  CLICommand,
+  createMultipleServers,
   doubleFollow,
-  execCLI,
-  flushAndRunMultipleServers,
-  getAccount,
-  getEnvCli,
   killallServers,
   makeGetRequest,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
   setDefaultVideoChannel,
-  updateMyAvatar,
-  uploadVideo,
-  wait
-} from '../../../shared/extra-utils'
-import { waitJobs } from '../../../shared/extra-utils/server/jobs'
-import { Account, VideoPlaylistPrivacy } from '../../../shared/models'
+  wait,
+  waitJobs
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
-async function countFiles (internalServerNumber: number, directory: string) {
-  const files = await readdir(buildServerDirectory({ internalServerNumber }, directory))
+async function countFiles (server: PeerTubeServer, directory: string) {
+  const files = await readdir(server.servers.buildDirectory(directory))
 
   return files.length
 }
 
-async function assertNotExists (internalServerNumber: number, directory: string, substring: string) {
-  const files = await readdir(buildServerDirectory({ internalServerNumber }, directory))
+async function assertNotExists (server: PeerTubeServer, directory: string, substring: string) {
+  const files = await readdir(server.servers.buildDirectory(directory))
 
   for (const f of files) {
     expect(f).to.not.contain(substring)
   }
 }
 
-async function assertCountAreOkay (servers: ServerInfo[]) {
+async function assertCountAreOkay (servers: PeerTubeServer[], videoServer2UUID: string) {
   for (const server of servers) {
-    const videosCount = await countFiles(server.internalServerNumber, 'videos')
+    const videosCount = await countFiles(server, 'videos')
     expect(videosCount).to.equal(8)
 
-    const torrentsCount = await countFiles(server.internalServerNumber, 'torrents')
+    const torrentsCount = await countFiles(server, 'torrents')
     expect(torrentsCount).to.equal(16)
 
-    const previewsCount = await countFiles(server.internalServerNumber, 'previews')
+    const previewsCount = await countFiles(server, 'previews')
     expect(previewsCount).to.equal(2)
 
-    const thumbnailsCount = await countFiles(server.internalServerNumber, 'thumbnails')
+    const thumbnailsCount = await countFiles(server, 'thumbnails')
     expect(thumbnailsCount).to.equal(6)
 
-    const avatarsCount = await countFiles(server.internalServerNumber, 'avatars')
+    const avatarsCount = await countFiles(server, 'avatars')
     expect(avatarsCount).to.equal(2)
   }
+
+  // When we'll prune HLS directories too
+  // const hlsRootCount = await countFiles(servers[1], 'streaming-playlists/hls/')
+  // expect(hlsRootCount).to.equal(2)
+
+  // const hlsCount = await countFiles(servers[1], 'streaming-playlists/hls/' + videoServer2UUID)
+  // expect(hlsCount).to.equal(10)
 }
 
 describe('Test prune storage scripts', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   const badNames: { [directory: string]: string[] } = {}
 
+  let videoServer2UUID: string
+
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: true } })
+    servers = await createMultipleServers(2, { transcoding: { enabled: true } })
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
 
     for (const server of servers) {
-      await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
-      await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
+      await server.videos.upload({ attributes: { name: 'video 1' } })
 
-      await updateMyAvatar({ url: server.url, accessToken: server.accessToken, fixture: 'avatar.png' })
+      const { uuid } = await server.videos.upload({ attributes: { name: 'video 2' } })
+      if (server.serverNumber === 2) videoServer2UUID = uuid
 
-      await createVideoPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
+      await server.users.updateMyAvatar({ fixture: 'avatar.png' })
+
+      await server.playlists.create({
+        attributes: {
           displayName: 'playlist',
           privacy: VideoPlaylistPrivacy.PUBLIC,
-          videoChannelId: server.videoChannel.id,
+          videoChannelId: server.store.channel.id,
           thumbnailfile: 'thumbnail.jpg'
         }
       })
@@ -95,41 +97,39 @@ describe('Test prune storage scripts', function () {
 
     // Lazy load the remote avatar
     {
-      const res = await getAccount(servers[0].url, 'root@localhost:' + servers[1].port)
-      const account: Account = res.body
+      const account = await servers[0].accounts.get({ accountName: 'root@localhost:' + servers[1].port })
       await makeGetRequest({
         url: servers[0].url,
         path: account.avatar.path,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     }
 
     {
-      const res = await getAccount(servers[1].url, 'root@localhost:' + servers[0].port)
-      const account: Account = res.body
+      const account = await servers[1].accounts.get({ accountName: 'root@localhost:' + servers[0].port })
       await makeGetRequest({
         url: servers[1].url,
         path: account.avatar.path,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
     }
 
     await wait(1000)
 
     await waitJobs(servers)
-    killallServers(servers)
+    await killallServers(servers)
 
     await wait(1000)
   })
 
   it('Should have the files on the disk', async function () {
-    await assertCountAreOkay(servers)
+    await assertCountAreOkay(servers, videoServer2UUID)
   })
 
   it('Should create some dirty files', async function () {
     for (let i = 0; i < 2; i++) {
       {
-        const base = buildServerDirectory(servers[0], 'videos')
+        const base = servers[0].servers.buildDirectory('videos')
 
         const n1 = buildUUID() + '.mp4'
         const n2 = buildUUID() + '.webm'
@@ -141,7 +141,7 @@ describe('Test prune storage scripts', function () {
       }
 
       {
-        const base = buildServerDirectory(servers[0], 'torrents')
+        const base = servers[0].servers.buildDirectory('torrents')
 
         const n1 = buildUUID() + '-240.torrent'
         const n2 = buildUUID() + '-480.torrent'
@@ -153,7 +153,7 @@ describe('Test prune storage scripts', function () {
       }
 
       {
-        const base = buildServerDirectory(servers[0], 'thumbnails')
+        const base = servers[0].servers.buildDirectory('thumbnails')
 
         const n1 = buildUUID() + '.jpg'
         const n2 = buildUUID() + '.jpg'
@@ -165,7 +165,7 @@ describe('Test prune storage scripts', function () {
       }
 
       {
-        const base = buildServerDirectory(servers[0], 'previews')
+        const base = servers[0].servers.buildDirectory('previews')
 
         const n1 = buildUUID() + '.jpg'
         const n2 = buildUUID() + '.jpg'
@@ -177,7 +177,7 @@ describe('Test prune storage scripts', function () {
       }
 
       {
-        const base = buildServerDirectory(servers[0], 'avatars')
+        const base = servers[0].servers.buildDirectory('avatars')
 
         const n1 = buildUUID() + '.png'
         const n2 = buildUUID() + '.jpg'
@@ -187,22 +187,44 @@ describe('Test prune storage scripts', function () {
 
         badNames['avatars'] = [ n1, n2 ]
       }
+
+      // When we'll prune HLS directories too
+      // {
+      //   const directory = join('streaming-playlists', 'hls')
+      //   const base = servers[1].servers.buildDirectory(directory)
+
+      //   const n1 = buildUUID()
+      //   await createFile(join(base, n1))
+      //   badNames[directory] = [ n1 ]
+      // }
+
+      // {
+      //   const directory = join('streaming-playlists', 'hls', videoServer2UUID)
+      //   const base = servers[1].servers.buildDirectory(directory)
+      //   const n1 = buildUUID() + '-240-fragmented-.mp4'
+      //   const n2 = buildUUID() + '-master.m3u8'
+
+      //   await createFile(join(base, n1))
+      //   await createFile(join(base, n2))
+
+      //   badNames[directory] = [ n1, n2 ]
+      // }
     }
   })
 
   it('Should run prune storage', async function () {
     this.timeout(30000)
 
-    const env = getEnvCli(servers[0])
-    await execCLI(`echo y | ${env} npm run prune-storage`)
+    const env = servers[0].cli.getEnv()
+    await CLICommand.exec(`echo y | ${env} npm run prune-storage`)
   })
 
   it('Should have removed files', async function () {
-    await assertCountAreOkay(servers)
+    await assertCountAreOkay(servers, videoServer2UUID)
 
     for (const directory of Object.keys(badNames)) {
       for (const name of badNames[directory]) {
-        await assertNotExists(servers[0].internalServerNumber, directory, name)
+        await assertNotExists(servers[0], directory, name)
       }
     }
   })
index 8acb9f263b2b649dd926b5f46e7cd4d20e98b647..780c9b4bd839c31e84af72b476d4200be9b2f0c4 100644 (file)
@@ -2,36 +2,33 @@ import 'mocha'
 import { expect } from 'chai'
 import { writeFile } from 'fs-extra'
 import { basename, join } from 'path'
-import { Video, VideoDetails } from '@shared/models'
+import { HttpStatusCode, Video } from '@shared/models'
 import {
-  buildServerDirectory,
   cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  execCLI,
-  flushAndRunMultipleServers,
-  getEnvCli,
-  getVideo,
   makeRawRequest,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  uploadVideoAndGetId,
   waitJobs
 } from '../../../shared/extra-utils'
-import { HttpStatusCode } from '@shared/core-utils'
 
-async function testThumbnail (server: ServerInfo, videoId: number | string) {
-  const res = await getVideo(server.url, videoId)
-  const video: VideoDetails = res.body
+async function testThumbnail (server: PeerTubeServer, videoId: number | string) {
+  const video = await server.videos.get({ id: videoId })
 
-  const res1 = await makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200)
-  expect(res1.body).to.not.have.lengthOf(0)
+  const requests = [
+    makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200),
+    makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200)
+  ]
 
-  const res2 = await makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200)
-  expect(res2.body).to.not.have.lengthOf(0)
+  for (const req of requests) {
+    const res = await req
+    expect(res.body).to.not.have.lengthOf(0)
+  }
 }
 
 describe('Test regenerate thumbnails script', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
 
   let video1: Video
   let video2: Video
@@ -43,28 +40,28 @@ describe('Test regenerate thumbnails script', function () {
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
 
     await doubleFollow(servers[0], servers[1])
 
     {
-      const videoUUID1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).uuid
-      video1 = await (getVideo(servers[0].url, videoUUID1).then(res => res.body))
+      const videoUUID1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).uuid
+      video1 = await servers[0].videos.get({ id: videoUUID1 })
 
-      thumbnail1Path = join(buildServerDirectory(servers[0], 'thumbnails'), basename(video1.thumbnailPath))
+      thumbnail1Path = join(servers[0].servers.buildDirectory('thumbnails'), basename(video1.thumbnailPath))
 
-      const videoUUID2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).uuid
-      video2 = await (getVideo(servers[0].url, videoUUID2).then(res => res.body))
+      const videoUUID2 = (await servers[0].videos.quickUpload({ name: 'video 2' })).uuid
+      video2 = await servers[0].videos.get({ id: videoUUID2 })
     }
 
     {
-      const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 3' })).uuid
+      const videoUUID = (await servers[1].videos.quickUpload({ name: 'video 3' })).uuid
       await waitJobs(servers)
 
-      remoteVideo = await (getVideo(servers[0].url, videoUUID).then(res => res.body))
+      remoteVideo = await servers[0].videos.get({ id: videoUUID })
 
-      thumbnailRemotePath = join(buildServerDirectory(servers[0], 'thumbnails'), basename(remoteVideo.thumbnailPath))
+      thumbnailRemotePath = join(servers[0].servers.buildDirectory('thumbnails'), basename(remoteVideo.thumbnailPath))
     }
 
     await writeFile(thumbnail1Path, '')
@@ -91,8 +88,7 @@ describe('Test regenerate thumbnails script', function () {
   it('Should regenerate local thumbnails from the CLI', async function () {
     this.timeout(15000)
 
-    const env = getEnvCli(servers[0])
-    await execCLI(`${env} npm run regenerate-thumbnails`)
+    await servers[0].cli.execWithEnv(`npm run regenerate-thumbnails`)
   })
 
   it('Should have generated new thumbnail files', async function () {
index a84463b331fbd05ef5b83431b4c93427b8e6c3f7..4a02db35d58a0dafb029a565da87ef76f6d48247 100644 (file)
@@ -1,35 +1,24 @@
 import 'mocha'
-
-import {
-  cleanupTests,
-  createUser,
-  execCLI,
-  flushAndRunServer,
-  getEnvCli,
-  login,
-  ServerInfo,
-  setAccessTokensToServers
-} from '../../../shared/extra-utils'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { cleanupTests, CLICommand, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '../../../shared/extra-utils'
 
 describe('Test reset password scripts', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   before(async function () {
     this.timeout(30000)
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
-    await createUser({ url: server.url, accessToken: server.accessToken, username: 'user_1', password: 'super password' })
+    await server.users.create({ username: 'user_1', password: 'super password' })
   })
 
   it('Should change the user password from CLI', async function () {
     this.timeout(60000)
 
-    const env = getEnvCli(server)
-    await execCLI(`echo coucou | ${env} npm run reset-password -- -u user_1`)
+    const env = server.cli.getEnv()
+    await CLICommand.exec(`echo coucou | ${env} npm run reset-password -- -u user_1`)
 
-    await login(server.url, server.client, { username: 'user_1', password: 'coucou' }, HttpStatusCode.OK_200)
+    await server.login.login({ user: { username: 'user_1', password: 'coucou' } })
   })
 
   after(async function () {
index 2070f16f57176c456a6f8f9ec2ed103bc226b986..43fbaec305d8489322a1008546d3c3cdc78b26fc 100644 (file)
@@ -1,33 +1,20 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import * as chai from 'chai'
-import { VideoDetails } from '../../../shared/models/videos'
-import { waitJobs } from '../../../shared/extra-utils/server/jobs'
-import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments'
+import { expect } from 'chai'
 import {
-  addVideoChannel,
   cleanupTests,
-  createUser,
-  execCLI,
-  flushAndRunServer,
-  getEnvCli,
-  getVideo,
-  getVideoChannelsList,
-  getVideosList,
+  createSingleServer,
   killallServers,
   makeActivityPubGetRequest,
-  parseTorrentVideo, reRunServer,
-  ServerInfo,
+  parseTorrentVideo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  uploadVideo
-} from '../../../shared/extra-utils'
-import { getAccountsList } from '../../../shared/extra-utils/users/accounts'
-
-const expect = chai.expect
+  waitJobs
+} from '@shared/extra-utils'
 
 describe('Test update host scripts', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   before(async function () {
     this.timeout(60000)
@@ -38,17 +25,15 @@ describe('Test update host scripts', function () {
       }
     }
     // Run server 2 to have transcoding enabled
-    server = await flushAndRunServer(2, overrideConfig)
+    server = await createSingleServer(2, overrideConfig)
     await setAccessTokensToServers([ server ])
 
     // Upload two videos for our needs
-    const videoAttributes = {}
-    const resVideo1 = await uploadVideo(server.url, server.accessToken, videoAttributes)
-    const video1UUID = resVideo1.body.video.uuid
-    await uploadVideo(server.url, server.accessToken, videoAttributes)
+    const { uuid: video1UUID } = await server.videos.upload()
+    await server.videos.upload()
 
     // Create a user
-    await createUser({ url: server.url, accessToken: server.accessToken, username: 'toto', password: 'coucou' })
+    await server.users.create({ username: 'toto', password: 'coucou' })
 
     // Create channel
     const videoChannel = {
@@ -56,11 +41,11 @@ describe('Test update host scripts', function () {
       displayName: 'second video channel',
       description: 'super video channel description'
     }
-    await addVideoChannel(server.url, server.accessToken, videoChannel)
+    await server.channels.create({ attributes: videoChannel })
 
     // Create comments
     const text = 'my super first comment'
-    await addVideoCommentThread(server.url, server.accessToken, video1UUID, text)
+    await server.comments.createThread({ videoId: video1UUID, text })
 
     await waitJobs(server)
   })
@@ -68,25 +53,23 @@ describe('Test update host scripts', function () {
   it('Should run update host', async function () {
     this.timeout(30000)
 
-    killallServers([ server ])
+    await killallServers([ server ])
     // Run server with standard configuration
-    await reRunServer(server)
+    await server.run()
 
-    const env = getEnvCli(server)
-    await execCLI(`${env} npm run update-host`)
+    await server.cli.execWithEnv(`npm run update-host`)
   })
 
   it('Should have updated videos url', async function () {
-    const res = await getVideosList(server.url)
-    expect(res.body.total).to.equal(2)
+    const { total, data } = await server.videos.list()
+    expect(total).to.equal(2)
 
-    for (const video of res.body.data) {
+    for (const video of data) {
       const { body } = await makeActivityPubGetRequest(server.url, '/videos/watch/' + video.uuid)
 
       expect(body.id).to.equal('http://localhost:9002/videos/watch/' + video.uuid)
 
-      const res = await getVideo(server.url, video.uuid)
-      const videoDetails: VideoDetails = res.body
+      const videoDetails = await server.videos.get({ id: video.uuid })
 
       expect(videoDetails.trackerUrls[0]).to.include(server.host)
       expect(videoDetails.streamingPlaylists[0].playlistUrl).to.include(server.host)
@@ -95,10 +78,10 @@ describe('Test update host scripts', function () {
   })
 
   it('Should have updated video channels url', async function () {
-    const res = await getVideoChannelsList(server.url, 0, 5, '-name')
-    expect(res.body.total).to.equal(3)
+    const { data, total } = await server.channels.list({ sort: '-name' })
+    expect(total).to.equal(3)
 
-    for (const channel of res.body.data) {
+    for (const channel of data) {
       const { body } = await makeActivityPubGetRequest(server.url, '/video-channels/' + channel.name)
 
       expect(body.id).to.equal('http://localhost:9002/video-channels/' + channel.name)
@@ -106,10 +89,10 @@ describe('Test update host scripts', function () {
   })
 
   it('Should have updated accounts url', async function () {
-    const res = await getAccountsList(server.url)
-    expect(res.body.total).to.equal(3)
+    const body = await server.accounts.list()
+    expect(body.total).to.equal(3)
 
-    for (const account of res.body.data) {
+    for (const account of body.data) {
       const usernameWithDomain = account.name
       const { body } = await makeActivityPubGetRequest(server.url, '/accounts/' + usernameWithDomain)
 
@@ -120,28 +103,27 @@ describe('Test update host scripts', function () {
   it('Should have updated torrent hosts', async function () {
     this.timeout(30000)
 
-    const res = await getVideosList(server.url)
-    const videos = res.body.data
-    expect(videos).to.have.lengthOf(2)
+    const { data } = await server.videos.list()
+    expect(data).to.have.lengthOf(2)
 
-    for (const video of videos) {
-      const res2 = await getVideo(server.url, video.id)
-      const videoDetails: VideoDetails = res2.body
+    for (const video of data) {
+      const videoDetails = await server.videos.get({ id: video.id })
+      const files = videoDetails.files.concat(videoDetails.streamingPlaylists[0].files)
 
-      expect(videoDetails.files).to.have.lengthOf(4)
+      expect(files).to.have.lengthOf(8)
 
-      for (const file of videoDetails.files) {
+      for (const file of files) {
         expect(file.magnetUri).to.contain('localhost%3A9002%2Ftracker%2Fsocket')
-        expect(file.magnetUri).to.contain('localhost%3A9002%2Fstatic%2Fwebseed%2F')
+        expect(file.magnetUri).to.contain('localhost%3A9002%2Fstatic%2F')
 
-        const torrent = await parseTorrentVideo(server, videoDetails.uuid, file.resolution.id)
+        const torrent = await parseTorrentVideo(server, file)
         const announceWS = torrent.announce.find(a => a === 'ws://localhost:9002/tracker/socket')
         expect(announceWS).to.not.be.undefined
 
         const announceHttp = torrent.announce.find(a => a === 'http://localhost:9002/tracker/announce')
         expect(announceHttp).to.not.be.undefined
 
-        expect(torrent.urlList[0]).to.contain('http://localhost:9002/static/webseed')
+        expect(torrent.urlList[0]).to.contain('http://localhost:9002/static/')
       }
     }
   })
index 7c4fb4e46c0976d66c382015dea09ddb8886305e..4cbdb2cb39e86fec125f262e155cb10646767ecc 100644 (file)
@@ -3,28 +3,16 @@
 import 'mocha'
 import * as chai from 'chai'
 import { omit } from 'lodash'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { Account, CustomConfig, HTMLServerConfig, ServerConfig, VideoPlaylistCreateResult, VideoPlaylistPrivacy } from '@shared/models'
+import { Account, HTMLServerConfig, HttpStatusCode, ServerConfig, VideoPlaylistCreateResult, VideoPlaylistPrivacy } from '@shared/models'
 import {
-  addVideoInPlaylist,
   cleanupTests,
-  createVideoPlaylist,
+  createMultipleServers,
   doubleFollow,
-  flushAndRunMultipleServers,
-  getAccount,
-  getConfig,
-  getCustomConfig,
-  getVideosList,
   makeGetRequest,
   makeHTMLRequest,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
   setDefaultVideoChannel,
-  updateCustomConfig,
-  updateCustomSubConfig,
-  updateMyUser,
-  updateVideoChannel,
-  uploadVideo,
   waitJobs
 } from '../../shared/extra-utils'
 
@@ -40,7 +28,7 @@ function checkIndexTags (html: string, title: string, description: string, css:
 }
 
 describe('Test a client controllers', function () {
-  let servers: ServerInfo[] = []
+  let servers: PeerTubeServer[] = []
   let account: Account
 
   const videoName = 'my super name for server 1'
@@ -62,7 +50,7 @@ describe('Test a client controllers', function () {
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
 
     await setAccessTokensToServers(servers)
 
@@ -70,47 +58,48 @@ describe('Test a client controllers', function () {
 
     await setDefaultVideoChannel(servers)
 
-    await updateVideoChannel(servers[0].url, servers[0].accessToken, servers[0].videoChannel.name, { description: channelDescription })
+    await servers[0].channels.update({
+      channelName: servers[0].store.channel.name,
+      attributes: { description: channelDescription }
+    })
 
     // Video
 
-    const videoAttributes = { name: videoName, description: videoDescription }
-    await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
+    {
+      const attributes = { name: videoName, description: videoDescription }
+      await servers[0].videos.upload({ attributes })
 
-    const resVideosRequest = await getVideosList(servers[0].url)
-    const videos = resVideosRequest.body.data
-    expect(videos.length).to.equal(1)
+      const { data } = await servers[0].videos.list()
+      expect(data.length).to.equal(1)
 
-    const video = videos[0]
-    servers[0].video = video
-    videoIds = [ video.id, video.uuid, video.shortUUID ]
+      const video = data[0]
+      servers[0].store.video = video
+      videoIds = [ video.id, video.uuid, video.shortUUID ]
+    }
 
     // Playlist
 
-    const playlistAttrs = {
-      displayName: playlistName,
-      description: playlistDescription,
-      privacy: VideoPlaylistPrivacy.PUBLIC,
-      videoChannelId: servers[0].videoChannel.id
-    }
+    {
+      const attributes = {
+        displayName: playlistName,
+        description: playlistDescription,
+        privacy: VideoPlaylistPrivacy.PUBLIC,
+        videoChannelId: servers[0].store.channel.id
+      }
 
-    const resVideoPlaylistRequest = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs })
-    playlist = resVideoPlaylistRequest.body.videoPlaylist
-    playlistIds = [ playlist.id, playlist.shortUUID, playlist.uuid ]
+      playlist = await servers[0].playlists.create({ attributes })
+      playlistIds = [ playlist.id, playlist.shortUUID, playlist.uuid ]
 
-    await addVideoInPlaylist({
-      url: servers[0].url,
-      token: servers[0].accessToken,
-      playlistId: playlist.shortUUID,
-      elementAttrs: { videoId: video.id }
-    })
+      await servers[0].playlists.addElement({ playlistId: playlist.shortUUID, attributes: { videoId: servers[0].store.video.id } })
+    }
 
     // Account
 
-    await updateMyUser({ url: servers[0].url, accessToken: servers[0].accessToken, description: 'my account description' })
+    {
+      await servers[0].users.updateMe({ description: 'my account description' })
 
-    const resAccountRequest = await getAccount(servers[0].url, `${servers[0].user.username}@${servers[0].host}`)
-    account = resAccountRequest.body
+      account = await servers[0].accounts.get({ accountName: `${servers[0].store.user.username}@${servers[0].host}` })
+    }
 
     await waitJobs(servers)
   })
@@ -124,14 +113,14 @@ describe('Test a client controllers', function () {
             url: servers[0].url,
             path: basePath + id,
             accept: 'text/html',
-            statusCodeExpected: HttpStatusCode.OK_200
+            expectedStatus: HttpStatusCode.OK_200
           })
 
           const port = servers[0].port
 
           const expectedLink = '<link rel="alternate" type="application/json+oembed" href="http://localhost:' + port + '/services/oembed?' +
-            `url=http%3A%2F%2Flocalhost%3A${port}%2Fw%2F${servers[0].video.uuid}" ` +
-            `title="${servers[0].video.name}" />`
+            `url=http%3A%2F%2Flocalhost%3A${port}%2Fw%2F${servers[0].store.video.shortUUID}" ` +
+            `title="${servers[0].store.video.name}" />`
 
           expect(res.text).to.contain(expectedLink)
         }
@@ -145,13 +134,13 @@ describe('Test a client controllers', function () {
             url: servers[0].url,
             path: basePath + id,
             accept: 'text/html',
-            statusCodeExpected: HttpStatusCode.OK_200
+            expectedStatus: HttpStatusCode.OK_200
           })
 
           const port = servers[0].port
 
           const expectedLink = '<link rel="alternate" type="application/json+oembed" href="http://localhost:' + port + '/services/oembed?' +
-            `url=http%3A%2F%2Flocalhost%3A${port}%2Fw%2Fp%2F${playlist.uuid}" ` +
+            `url=http%3A%2F%2Flocalhost%3A${port}%2Fw%2Fp%2F${playlist.shortUUID}" ` +
             `title="${playlistName}" />`
 
           expect(res.text).to.contain(expectedLink)
@@ -163,55 +152,55 @@ describe('Test a client controllers', function () {
   describe('Open Graph', function () {
 
     async function accountPageTest (path: string) {
-      const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+      const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
       const text = res.text
 
       expect(text).to.contain(`<meta property="og:title" content="${account.displayName}" />`)
       expect(text).to.contain(`<meta property="og:description" content="${account.description}" />`)
       expect(text).to.contain('<meta property="og:type" content="website" />')
-      expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/accounts/${servers[0].user.username}" />`)
+      expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/accounts/${servers[0].store.user.username}" />`)
     }
 
     async function channelPageTest (path: string) {
-      const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+      const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
       const text = res.text
 
-      expect(text).to.contain(`<meta property="og:title" content="${servers[0].videoChannel.displayName}" />`)
+      expect(text).to.contain(`<meta property="og:title" content="${servers[0].store.channel.displayName}" />`)
       expect(text).to.contain(`<meta property="og:description" content="${channelDescription}" />`)
       expect(text).to.contain('<meta property="og:type" content="website" />')
-      expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/video-channels/${servers[0].videoChannel.name}" />`)
+      expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/video-channels/${servers[0].store.channel.name}" />`)
     }
 
     async function watchVideoPageTest (path: string) {
-      const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+      const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
       const text = res.text
 
       expect(text).to.contain(`<meta property="og:title" content="${videoName}" />`)
       expect(text).to.contain(`<meta property="og:description" content="${videoDescriptionPlainText}" />`)
       expect(text).to.contain('<meta property="og:type" content="video" />')
-      expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/${servers[0].video.uuid}" />`)
+      expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/${servers[0].store.video.shortUUID}" />`)
     }
 
     async function watchPlaylistPageTest (path: string) {
-      const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+      const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
       const text = res.text
 
       expect(text).to.contain(`<meta property="og:title" content="${playlistName}" />`)
       expect(text).to.contain(`<meta property="og:description" content="${playlistDescription}" />`)
       expect(text).to.contain('<meta property="og:type" content="video" />')
-      expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/p/${playlist.uuid}" />`)
+      expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/p/${playlist.shortUUID}" />`)
     }
 
     it('Should have valid Open Graph tags on the account page', async function () {
-      await accountPageTest('/accounts/' + servers[0].user.username)
-      await accountPageTest('/a/' + servers[0].user.username)
-      await accountPageTest('/@' + servers[0].user.username)
+      await accountPageTest('/accounts/' + servers[0].store.user.username)
+      await accountPageTest('/a/' + servers[0].store.user.username)
+      await accountPageTest('/@' + servers[0].store.user.username)
     })
 
     it('Should have valid Open Graph tags on the channel page', async function () {
-      await channelPageTest('/video-channels/' + servers[0].videoChannel.name)
-      await channelPageTest('/c/' + servers[0].videoChannel.name)
-      await channelPageTest('/@' + servers[0].videoChannel.name)
+      await channelPageTest('/video-channels/' + servers[0].store.channel.name)
+      await channelPageTest('/c/' + servers[0].store.channel.name)
+      await channelPageTest('/@' + servers[0].store.channel.name)
     })
 
     it('Should have valid Open Graph tags on the watch page', async function () {
@@ -236,7 +225,7 @@ describe('Test a client controllers', function () {
     describe('Not whitelisted', function () {
 
       async function accountPageTest (path: string) {
-        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
         const text = res.text
 
         expect(text).to.contain('<meta property="twitter:card" content="summary" />')
@@ -246,17 +235,17 @@ describe('Test a client controllers', function () {
       }
 
       async function channelPageTest (path: string) {
-        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
         const text = res.text
 
         expect(text).to.contain('<meta property="twitter:card" content="summary" />')
         expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
-        expect(text).to.contain(`<meta property="twitter:title" content="${servers[0].videoChannel.displayName}" />`)
+        expect(text).to.contain(`<meta property="twitter:title" content="${servers[0].store.channel.displayName}" />`)
         expect(text).to.contain(`<meta property="twitter:description" content="${channelDescription}" />`)
       }
 
       async function watchVideoPageTest (path: string) {
-        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
         const text = res.text
 
         expect(text).to.contain('<meta property="twitter:card" content="summary_large_image" />')
@@ -266,7 +255,7 @@ describe('Test a client controllers', function () {
       }
 
       async function watchPlaylistPageTest (path: string) {
-        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
         const text = res.text
 
         expect(text).to.contain('<meta property="twitter:card" content="summary" />')
@@ -298,27 +287,26 @@ describe('Test a client controllers', function () {
       })
 
       it('Should have valid twitter card on the channel page', async function () {
-        await channelPageTest('/video-channels/' + servers[0].videoChannel.name)
-        await channelPageTest('/c/' + servers[0].videoChannel.name)
-        await channelPageTest('/@' + servers[0].videoChannel.name)
+        await channelPageTest('/video-channels/' + servers[0].store.channel.name)
+        await channelPageTest('/c/' + servers[0].store.channel.name)
+        await channelPageTest('/@' + servers[0].store.channel.name)
       })
     })
 
     describe('Whitelisted', function () {
 
       before(async function () {
-        const res = await getCustomConfig(servers[0].url, servers[0].accessToken)
-        const config = res.body as CustomConfig
+        const config = await servers[0].config.getCustomConfig()
         config.services.twitter = {
           username: '@Kuja',
           whitelisted: true
         }
 
-        await updateCustomConfig(servers[0].url, servers[0].accessToken, config)
+        await servers[0].config.updateCustomConfig({ newCustomConfig: config })
       })
 
       async function accountPageTest (path: string) {
-        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
         const text = res.text
 
         expect(text).to.contain('<meta property="twitter:card" content="summary" />')
@@ -326,7 +314,7 @@ describe('Test a client controllers', function () {
       }
 
       async function channelPageTest (path: string) {
-        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
         const text = res.text
 
         expect(text).to.contain('<meta property="twitter:card" content="summary" />')
@@ -334,7 +322,7 @@ describe('Test a client controllers', function () {
       }
 
       async function watchVideoPageTest (path: string) {
-        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
         const text = res.text
 
         expect(text).to.contain('<meta property="twitter:card" content="player" />')
@@ -342,7 +330,7 @@ describe('Test a client controllers', function () {
       }
 
       async function watchPlaylistPageTest (path: string) {
-        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
+        const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
         const text = res.text
 
         expect(text).to.contain('<meta property="twitter:card" content="player" />')
@@ -372,9 +360,9 @@ describe('Test a client controllers', function () {
       })
 
       it('Should have valid twitter card on the channel page', async function () {
-        await channelPageTest('/video-channels/' + servers[0].videoChannel.name)
-        await channelPageTest('/c/' + servers[0].videoChannel.name)
-        await channelPageTest('/@' + servers[0].videoChannel.name)
+        await channelPageTest('/video-channels/' + servers[0].store.channel.name)
+        await channelPageTest('/c/' + servers[0].store.channel.name)
+        await channelPageTest('/@' + servers[0].store.channel.name)
       })
     })
   })
@@ -382,53 +370,55 @@ describe('Test a client controllers', function () {
   describe('Index HTML', function () {
 
     it('Should have valid index html tags (title, description...)', async function () {
-      const resConfig = await getConfig(servers[0].url)
+      const config = await servers[0].config.getConfig()
       const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
 
       const description = 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.'
-      checkIndexTags(res.text, 'PeerTube', description, '', resConfig.body)
+      checkIndexTags(res.text, 'PeerTube', description, '', config)
     })
 
     it('Should update the customized configuration and have the correct index html tags', async function () {
-      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-        instance: {
-          name: 'PeerTube updated',
-          shortDescription: 'my short description',
-          description: 'my super description',
-          terms: 'my super terms',
-          defaultNSFWPolicy: 'blur',
-          defaultClientRoute: '/videos/recently-added',
-          customizations: {
-            javascript: 'alert("coucou")',
-            css: 'body { background-color: red; }'
+      await servers[0].config.updateCustomSubConfig({
+        newConfig: {
+          instance: {
+            name: 'PeerTube updated',
+            shortDescription: 'my short description',
+            description: 'my super description',
+            terms: 'my super terms',
+            defaultNSFWPolicy: 'blur',
+            defaultClientRoute: '/videos/recently-added',
+            customizations: {
+              javascript: 'alert("coucou")',
+              css: 'body { background-color: red; }'
+            }
           }
         }
       })
 
-      const resConfig = await getConfig(servers[0].url)
+      const config = await servers[0].config.getConfig()
       const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
 
-      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
+      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config)
     })
 
     it('Should have valid index html updated tags (title, description...)', async function () {
-      const resConfig = await getConfig(servers[0].url)
+      const config = await servers[0].config.getConfig()
       const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
 
-      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
+      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config)
     })
 
     it('Should use the original video URL for the canonical tag', async function () {
       for (const basePath of watchVideoBasePaths) {
         for (const id of videoIds) {
           const res = await makeHTMLRequest(servers[1].url, basePath + id)
-          expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/videos/watch/${servers[0].video.uuid}" />`)
+          expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/videos/watch/${servers[0].store.video.uuid}" />`)
         }
       }
     })
 
     it('Should use the original account URL for the canonical tag', async function () {
-      const accountURLtest = (res) => {
+      const accountURLtest = res => {
         expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/accounts/root" />`)
       }
 
@@ -438,7 +428,7 @@ describe('Test a client controllers', function () {
     })
 
     it('Should use the original channel URL for the canonical tag', async function () {
-      const channelURLtests = (res) => {
+      const channelURLtests = res => {
         expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-channels/root_channel" />`)
       }
 
@@ -460,10 +450,10 @@ describe('Test a client controllers', function () {
   describe('Embed HTML', function () {
 
     it('Should have the correct embed html tags', async function () {
-      const resConfig = await getConfig(servers[0].url)
-      const res = await makeHTMLRequest(servers[0].url, servers[0].video.embedPath)
+      const config = await servers[0].config.getConfig()
+      const res = await makeHTMLRequest(servers[0].url, servers[0].store.video.embedPath)
 
-      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
+      checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config)
     })
   })
 
index e4eae7e8c047b858154fcb598ada2838913609ed..acec69df59e455f9a36d3859b53aee2f8ed2b9dc 100644 (file)
@@ -2,46 +2,29 @@
 
 import 'mocha'
 import { expect } from 'chai'
-import { User } from '@shared/models/users/user.model'
-import {
-  blockUser,
-  getMyUserInformation,
-  installPlugin,
-  setAccessTokensToServers,
-  unblockUser,
-  uninstallPlugin,
-  updatePluginSettings,
-  uploadVideo,
-  userLogin
-} from '../../../shared/extra-utils'
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
+import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Official plugin auth-ldap', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   let accessToken: string
   let userId: number
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-plugin-auth-ldap'
-    })
+    await server.plugins.install({ npmName: 'peertube-plugin-auth-ldap' })
   })
 
   it('Should not login with without LDAP settings', async function () {
-    await userLogin(server, { username: 'fry', password: 'fry' }, 400)
+    await server.login.login({ user: { username: 'fry', password: 'fry' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should not login with bad LDAP settings', async function () {
-    await updatePluginSettings({
-      url: server.url,
-      accessToken: server.accessToken,
+    await server.plugins.updateSettings({
       npmName: 'peertube-plugin-auth-ldap',
       settings: {
         'bind-credentials': 'GoodNewsEveryone',
@@ -55,13 +38,11 @@ describe('Official plugin auth-ldap', function () {
       }
     })
 
-    await userLogin(server, { username: 'fry', password: 'fry' }, 400)
+    await server.login.login({ user: { username: 'fry', password: 'fry' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should not login with good LDAP settings but wrong username/password', async function () {
-    await updatePluginSettings({
-      url: server.url,
-      accessToken: server.accessToken,
+    await server.plugins.updateSettings({
       npmName: 'peertube-plugin-auth-ldap',
       settings: {
         'bind-credentials': 'GoodNewsEveryone',
@@ -75,22 +56,20 @@ describe('Official plugin auth-ldap', function () {
       }
     })
 
-    await userLogin(server, { username: 'fry', password: 'bad password' }, 400)
-    await userLogin(server, { username: 'fryr', password: 'fry' }, 400)
+    await server.login.login({ user: { username: 'fry', password: 'bad password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await server.login.login({ user: { username: 'fryr', password: 'fry' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should login with the appropriate username/password', async function () {
-    accessToken = await userLogin(server, { username: 'fry', password: 'fry' })
+    accessToken = await server.login.getAccessToken({ username: 'fry', password: 'fry' })
   })
 
   it('Should login with the appropriate email/password', async function () {
-    accessToken = await userLogin(server, { username: 'fry@planetexpress.com', password: 'fry' })
+    accessToken = await server.login.getAccessToken({ username: 'fry@planetexpress.com', password: 'fry' })
   })
 
   it('Should login get my profile', async function () {
-    const res = await getMyUserInformation(server.url, accessToken)
-    const body: User = res.body
-
+    const body = await server.users.getMyInfo({ token: accessToken })
     expect(body.username).to.equal('fry')
     expect(body.email).to.equal('fry@planetexpress.com')
 
@@ -98,25 +77,31 @@ describe('Official plugin auth-ldap', function () {
   })
 
   it('Should upload a video', async function () {
-    await uploadVideo(server.url, accessToken, { name: 'my super video' })
+    await server.videos.upload({ token: accessToken, attributes: { name: 'my super video' } })
   })
 
   it('Should not be able to login if the user is banned', async function () {
-    await blockUser(server.url, userId, server.accessToken)
+    await server.users.banUser({ userId })
 
-    await userLogin(server, { username: 'fry@planetexpress.com', password: 'fry' }, 400)
+    await server.login.login({
+      user: { username: 'fry@planetexpress.com', password: 'fry' },
+      expectedStatus: HttpStatusCode.BAD_REQUEST_400
+    })
   })
 
   it('Should be able to login if the user is unbanned', async function () {
-    await unblockUser(server.url, userId, server.accessToken)
+    await server.users.unbanUser({ userId })
 
-    await userLogin(server, { username: 'fry@planetexpress.com', password: 'fry' })
+    await server.login.login({ user: { username: 'fry@planetexpress.com', password: 'fry' } })
   })
 
   it('Should not login if the plugin is uninstalled', async function () {
-    await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-auth-ldap' })
+    await server.plugins.uninstall({ npmName: 'peertube-plugin-auth-ldap' })
 
-    await userLogin(server, { username: 'fry@planetexpress.com', password: 'fry' }, 400)
+    await server.login.login({
+      user: { username: 'fry@planetexpress.com', password: 'fry' },
+      expectedStatus: HttpStatusCode.BAD_REQUEST_400
+    })
   })
 
   after(async function () {
index 18ea17d78c8e5ab89034b1e0ece56dac42a4c95c..0eb4bda9ade8e0e9c007419cd4f9f81411e0af64 100644 (file)
@@ -2,41 +2,29 @@
 
 import 'mocha'
 import { expect } from 'chai'
-import { Video, VideoBlacklist } from '@shared/models'
 import {
+  cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  getBlacklistedVideosList,
-  getVideosList,
-  installPlugin,
+  killallServers,
   MockBlocklist,
-  removeVideoFromBlacklist,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updatePluginSettings,
-  uploadVideoAndGetId,
   wait
-} from '../../../shared/extra-utils'
-import {
-  cleanupTests,
-  flushAndRunMultipleServers,
-  killallServers,
-  reRunServer,
-  ServerInfo
-} from '../../../shared/extra-utils/server/servers'
+} from '@shared/extra-utils'
+import { Video } from '@shared/models'
 
-async function check (server: ServerInfo, videoUUID: string, exists = true) {
-  const res = await getVideosList(server.url)
+async function check (server: PeerTubeServer, videoUUID: string, exists = true) {
+  const { data } = await server.videos.list()
 
-  const video = res.body.data.find(v => v.uuid === videoUUID)
+  const video = data.find(v => v.uuid === videoUUID)
 
-  if (exists) {
-    expect(video).to.not.be.undefined
-  } else {
-    expect(video).to.be.undefined
-  }
+  if (exists) expect(video).to.not.be.undefined
+  else expect(video).to.be.undefined
 }
 
 describe('Official plugin auto-block videos', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let blocklistServer: MockBlocklist
   let server1Videos: Video[] = []
   let server2Videos: Video[] = []
@@ -45,42 +33,36 @@ describe('Official plugin auto-block videos', function () {
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
 
     for (const server of servers) {
-      await installPlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        npmName: 'peertube-plugin-auto-block-videos'
-      })
+      await server.plugins.install({ npmName: 'peertube-plugin-auto-block-videos' })
     }
 
     blocklistServer = new MockBlocklist()
     port = await blocklistServer.initialize()
 
-    await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' })
-    await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' })
-    await uploadVideoAndGetId({ server: servers[1], videoName: 'video 2 server 2' })
-    await uploadVideoAndGetId({ server: servers[1], videoName: 'video 3 server 2' })
+    await servers[0].videos.quickUpload({ name: 'video server 1' })
+    await servers[1].videos.quickUpload({ name: 'video server 2' })
+    await servers[1].videos.quickUpload({ name: 'video 2 server 2' })
+    await servers[1].videos.quickUpload({ name: 'video 3 server 2' })
 
     {
-      const res = await getVideosList(servers[0].url)
-      server1Videos = res.body.data.map(v => Object.assign(v, { url: servers[0].url + '/videos/watch/' + v.uuid }))
+      const { data } = await servers[0].videos.list()
+      server1Videos = data.map(v => Object.assign(v, { url: servers[0].url + '/videos/watch/' + v.uuid }))
     }
 
     {
-      const res = await getVideosList(servers[1].url)
-      server2Videos = res.body.data.map(v => Object.assign(v, { url: servers[1].url + '/videos/watch/' + v.uuid }))
+      const { data } = await servers[1].videos.list()
+      server2Videos = data.map(v => Object.assign(v, { url: servers[1].url + '/videos/watch/' + v.uuid }))
     }
 
     await doubleFollow(servers[0], servers[1])
   })
 
   it('Should update plugin settings', async function () {
-    await updatePluginSettings({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
+    await servers[0].plugins.updateSettings({
       npmName: 'peertube-plugin-auto-block-videos',
       settings: {
         'blocklist-urls': `http://localhost:${port}/blocklist`,
@@ -108,10 +90,9 @@ describe('Official plugin auto-block videos', function () {
   })
 
   it('Should have video in blacklists', async function () {
-    const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken })
-
-    const videoBlacklists = res.body.data as VideoBlacklist[]
+    const body = await servers[0].blacklist.list()
 
+    const videoBlacklists = body.data
     expect(videoBlacklists).to.have.lengthOf(1)
     expect(videoBlacklists[0].reason).to.contains('Automatically blocked from auto block plugin')
     expect(videoBlacklists[0].video.name).to.equal(server2Videos[0].name)
@@ -174,12 +155,12 @@ describe('Official plugin auto-block videos', function () {
 
     await check(servers[0], video.uuid, false)
 
-    await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, video.uuid)
+    await servers[0].blacklist.remove({ videoId: video.uuid })
 
     await check(servers[0], video.uuid, true)
 
-    killallServers([ servers[0] ])
-    await reRunServer(servers[0])
+    await killallServers([ servers[0] ])
+    await servers[0].run()
     await wait(2000)
 
     await check(servers[0], video.uuid, true)
index 09355d932697ad612919cf062e97e4822596d323..271779dd4c5eb99adfca4e269ccd0c7a6ad479e4 100644 (file)
@@ -3,63 +3,45 @@
 import 'mocha'
 import { expect } from 'chai'
 import {
-  addAccountToServerBlocklist,
-  addServerToAccountBlocklist,
-  removeAccountFromServerBlocklist
-} from '@shared/extra-utils/users/blocklist'
-import {
+  cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  getVideosList,
-  installPlugin,
+  killallServers,
   makeGetRequest,
   MockBlocklist,
+  PeerTubeServer,
   setAccessTokensToServers,
-  updatePluginSettings,
-  uploadVideoAndGetId,
   wait
-} from '../../../shared/extra-utils'
-import {
-  cleanupTests,
-  flushAndRunMultipleServers,
-  killallServers,
-  reRunServer,
-  ServerInfo
-} from '../../../shared/extra-utils/server/servers'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Official plugin auto-mute', function () {
   const autoMuteListPath = '/plugins/auto-mute/router/api/v1/mute-list'
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let blocklistServer: MockBlocklist
   let port: number
 
   before(async function () {
     this.timeout(30000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
 
     for (const server of servers) {
-      await installPlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        npmName: 'peertube-plugin-auto-mute'
-      })
+      await server.plugins.install({ npmName: 'peertube-plugin-auto-mute' })
     }
 
     blocklistServer = new MockBlocklist()
     port = await blocklistServer.initialize()
 
-    await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' })
-    await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' })
+    await servers[0].videos.quickUpload({ name: 'video server 1' })
+    await servers[1].videos.quickUpload({ name: 'video server 2' })
 
     await doubleFollow(servers[0], servers[1])
   })
 
   it('Should update plugin settings', async function () {
-    await updatePluginSettings({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
+    await servers[0].plugins.updateSettings({
       npmName: 'peertube-plugin-auto-mute',
       settings: {
         'blocklist-urls': `http://localhost:${port}/blocklist`,
@@ -81,8 +63,8 @@ describe('Official plugin auto-mute', function () {
 
     await wait(2000)
 
-    const res = await getVideosList(servers[0].url)
-    expect(res.body.total).to.equal(1)
+    const { total } = await servers[0].videos.list()
+    expect(total).to.equal(1)
   })
 
   it('Should remove a server blocklist', async function () {
@@ -99,8 +81,8 @@ describe('Official plugin auto-mute', function () {
 
     await wait(2000)
 
-    const res = await getVideosList(servers[0].url)
-    expect(res.body.total).to.equal(2)
+    const { total } = await servers[0].videos.list()
+    expect(total).to.equal(2)
   })
 
   it('Should add an account blocklist', async function () {
@@ -116,8 +98,8 @@ describe('Official plugin auto-mute', function () {
 
     await wait(2000)
 
-    const res = await getVideosList(servers[0].url)
-    expect(res.body.total).to.equal(1)
+    const { total } = await servers[0].videos.list()
+    expect(total).to.equal(1)
   })
 
   it('Should remove an account blocklist', async function () {
@@ -134,8 +116,8 @@ describe('Official plugin auto-mute', function () {
 
     await wait(2000)
 
-    const res = await getVideosList(servers[0].url)
-    expect(res.body.total).to.equal(2)
+    const { total } = await servers[0].videos.list()
+    expect(total).to.equal(2)
   })
 
   it('Should auto mute an account, manually unmute it and do not remute it automatically', async function () {
@@ -155,24 +137,24 @@ describe('Official plugin auto-mute', function () {
     await wait(2000)
 
     {
-      const res = await getVideosList(servers[0].url)
-      expect(res.body.total).to.equal(1)
+      const { total } = await servers[0].videos.list()
+      expect(total).to.equal(1)
     }
 
-    await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, account)
+    await servers[0].blocklist.removeFromServerBlocklist({ account })
 
     {
-      const res = await getVideosList(servers[0].url)
-      expect(res.body.total).to.equal(2)
+      const { total } = await servers[0].videos.list()
+      expect(total).to.equal(2)
     }
 
-    killallServers([ servers[0] ])
-    await reRunServer(servers[0])
+    await killallServers([ servers[0] ])
+    await servers[0].run()
     await wait(2000)
 
     {
-      const res = await getVideosList(servers[0].url)
-      expect(res.body.total).to.equal(2)
+      const { total } = await servers[0].videos.list()
+      expect(total).to.equal(2)
     }
   })
 
@@ -180,14 +162,12 @@ describe('Official plugin auto-mute', function () {
     await makeGetRequest({
       url: servers[0].url,
       path: '/plugins/auto-mute/router/api/v1/mute-list',
-      statusCodeExpected: HttpStatusCode.FORBIDDEN_403
+      expectedStatus: HttpStatusCode.FORBIDDEN_403
     })
   })
 
   it('Should enable auto mute list', async function () {
-    await updatePluginSettings({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
+    await servers[0].plugins.updateSettings({
       npmName: 'peertube-plugin-auto-mute',
       settings: {
         'blocklist-urls': '',
@@ -199,16 +179,14 @@ describe('Official plugin auto-mute', function () {
     await makeGetRequest({
       url: servers[0].url,
       path: '/plugins/auto-mute/router/api/v1/mute-list',
-      statusCodeExpected: HttpStatusCode.OK_200
+      expectedStatus: HttpStatusCode.OK_200
     })
   })
 
   it('Should mute an account on server 1, and server 2 auto mutes it', async function () {
     this.timeout(20000)
 
-    await updatePluginSettings({
-      url: servers[1].url,
-      accessToken: servers[1].accessToken,
+    await servers[1].plugins.updateSettings({
       npmName: 'peertube-plugin-auto-mute',
       settings: {
         'blocklist-urls': 'http://localhost:' + servers[0].port + autoMuteListPath,
@@ -217,13 +195,13 @@ describe('Official plugin auto-mute', function () {
       }
     })
 
-    await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'root@localhost:' + servers[1].port)
-    await addServerToAccountBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
+    await servers[0].blocklist.addToServerBlocklist({ account: 'root@localhost:' + servers[1].port })
+    await servers[0].blocklist.addToMyBlocklist({ server: 'localhost:' + servers[1].port })
 
     const res = await makeGetRequest({
       url: servers[0].url,
       path: '/plugins/auto-mute/router/api/v1/mute-list',
-      statusCodeExpected: HttpStatusCode.OK_200
+      expectedStatus: HttpStatusCode.OK_200
     })
 
     const data = res.body.data
@@ -234,8 +212,8 @@ describe('Official plugin auto-mute', function () {
     await wait(2000)
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-      expect(res.body.total).to.equal(1)
+      const { total } = await server.videos.list()
+      expect(total).to.equal(1)
     }
   })
 
index 7bad81751a393331574e0798de3d89cdfe218423..55b4348465516294ce48e73bde82ff35c6d88233 100644 (file)
@@ -3,35 +3,17 @@
 import 'mocha'
 import * as chai from 'chai'
 import * as xmlParser from 'fast-xml-parser'
-import {
-  addAccountToAccountBlocklist,
-  addAccountToServerBlocklist,
-  removeAccountFromServerBlocklist
-} from '@shared/extra-utils/users/blocklist'
-import { addUserSubscription, listUserSubscriptionVideos } from '@shared/extra-utils/users/user-subscriptions'
-import { VideoPrivacy } from '@shared/models'
-import { ScopedToken } from '@shared/models/users/user-scoped-token'
 import {
   cleanupTests,
-  createUser,
+  createMultipleServers,
+  createSingleServer,
   doubleFollow,
-  flushAndRunMultipleServers,
-  flushAndRunServer,
-  getJSONfeed,
-  getMyUserInformation,
-  getUserScopedTokens,
-  getXMLfeed,
-  renewUserScopedTokens,
-  ServerInfo,
+  makeGetRequest,
+  PeerTubeServer,
   setAccessTokensToServers,
-  uploadVideo,
-  uploadVideoAndGetId,
-  userLogin
-} from '../../../shared/extra-utils'
-import { waitJobs } from '../../../shared/extra-utils/server/jobs'
-import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments'
-import { User } from '../../../shared/models/users'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+  waitJobs
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoPrivacy } from '@shared/models'
 
 chai.use(require('chai-xml'))
 chai.use(require('chai-json-schema'))
@@ -39,8 +21,8 @@ chai.config.includeStack = true
 const expect = chai.expect
 
 describe('Test syndication feeds', () => {
-  let servers: ServerInfo[] = []
-  let serverHLSOnly: ServerInfo
+  let servers: PeerTubeServer[] = []
+  let serverHLSOnly: PeerTubeServer
   let userAccessToken: string
   let rootAccountId: number
   let rootChannelId: number
@@ -52,8 +34,8 @@ describe('Test syndication feeds', () => {
     this.timeout(120000)
 
     // Run servers
-    servers = await flushAndRunMultipleServers(2)
-    serverHLSOnly = await flushAndRunServer(3, {
+    servers = await createMultipleServers(2)
+    serverHLSOnly = await createSingleServer(3, {
       transcoding: {
         enabled: true,
         webtorrent: { enabled: false },
@@ -65,50 +47,43 @@ describe('Test syndication feeds', () => {
     await doubleFollow(servers[0], servers[1])
 
     {
-      const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
-      const user: User = res.body
+      const user = await servers[0].users.getMyInfo()
       rootAccountId = user.account.id
       rootChannelId = user.videoChannels[0].id
     }
 
     {
-      const attr = { username: 'john', password: 'password' }
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: attr.username, password: attr.password })
-      userAccessToken = await userLogin(servers[0], attr)
+      userAccessToken = await servers[0].users.generateUserAndToken('john')
 
-      const res = await getMyUserInformation(servers[0].url, userAccessToken)
-      const user: User = res.body
+      const user = await servers[0].users.getMyInfo({ token: userAccessToken })
       userAccountId = user.account.id
       userChannelId = user.videoChannels[0].id
 
-      const res2 = await getUserScopedTokens(servers[0].url, userAccessToken)
-      const token: ScopedToken = res2.body
+      const token = await servers[0].users.getMyScopedTokens({ token: userAccessToken })
       userFeedToken = token.feedToken
     }
 
     {
-      await uploadVideo(servers[0].url, userAccessToken, { name: 'user video' })
+      await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'user video' } })
     }
 
     {
-      const videoAttributes = {
+      const attributes = {
         name: 'my super name for server 1',
         description: 'my super description for server 1',
         fixture: 'video_short.webm'
       }
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
-      const videoId = res.body.video.id
+      const { id } = await servers[0].videos.upload({ attributes })
 
-      await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoId, 'super comment 1')
-      await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoId, 'super comment 2')
+      await servers[0].comments.createThread({ videoId: id, text: 'super comment 1' })
+      await servers[0].comments.createThread({ videoId: id, text: 'super comment 2' })
     }
 
     {
-      const videoAttributes = { name: 'unlisted video', privacy: VideoPrivacy.UNLISTED }
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
-      const videoId = res.body.video.id
+      const attributes = { name: 'unlisted video', privacy: VideoPrivacy.UNLISTED }
+      const { id } = await servers[0].videos.upload({ attributes })
 
-      await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoId, 'comment on unlisted video')
+      await servers[0].comments.createThread({ videoId: id, text: 'comment on unlisted video' })
     }
 
     await waitJobs(servers)
@@ -118,30 +93,65 @@ describe('Test syndication feeds', () => {
 
     it('Should be well formed XML (covers RSS 2.0 and ATOM 1.0 endpoints)', async function () {
       for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) {
-        const rss = await getXMLfeed(servers[0].url, feed)
-        expect(rss.text).xml.to.be.valid()
+        const rss = await servers[0].feed.getXML({ feed })
+        expect(rss).xml.to.be.valid()
 
-        const atom = await getXMLfeed(servers[0].url, feed, 'atom')
-        expect(atom.text).xml.to.be.valid()
+        const atom = await servers[0].feed.getXML({ feed, format: 'atom' })
+        expect(atom).xml.to.be.valid()
       }
     })
 
     it('Should be well formed JSON (covers JSON feed 1.0 endpoint)', async function () {
       for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) {
-        const json = await getJSONfeed(servers[0].url, feed)
-        expect(JSON.parse(json.text)).to.be.jsonSchema({ type: 'object' })
+        const jsonText = await servers[0].feed.getJSON({ feed })
+        expect(JSON.parse(jsonText)).to.be.jsonSchema({ type: 'object' })
       }
     })
+
+    it('Should serve the endpoint with a classic request', async function () {
+      await makeGetRequest({
+        url: servers[0].url,
+        path: '/feeds/videos.xml',
+        accept: 'application/xml',
+        expectedStatus: HttpStatusCode.OK_200
+      })
+    })
+
+    it('Should serve the endpoint as a cached request', async function () {
+      const res = await makeGetRequest({
+        url: servers[0].url,
+        path: '/feeds/videos.xml',
+        accept: 'application/xml',
+        expectedStatus: HttpStatusCode.OK_200
+      })
+
+      expect(res.headers['x-api-cache-cached']).to.equal('true')
+    })
+
+    it('Should not serve the endpoint as a cached request', async function () {
+      const res = await makeGetRequest({
+        url: servers[0].url,
+        path: '/feeds/videos.xml?v=186',
+        accept: 'application/xml',
+        expectedStatus: HttpStatusCode.OK_200
+      })
+
+      expect(res.headers['x-api-cache-cached']).to.not.exist
+    })
+
+    it('Should refuse to serve the endpoint without accept header', async function () {
+      await makeGetRequest({ url: servers[0].url, path: '/feeds/videos.xml', expectedStatus: HttpStatusCode.NOT_ACCEPTABLE_406 })
+    })
   })
 
   describe('Videos feed', function () {
 
     it('Should contain a valid enclosure (covers RSS 2.0 endpoint)', async function () {
       for (const server of servers) {
-        const rss = await getXMLfeed(server.url, 'videos')
-        expect(xmlParser.validate(rss.text)).to.be.true
+        const rss = await server.feed.getXML({ feed: 'videos' })
+        expect(xmlParser.validate(rss)).to.be.true
 
-        const xmlDoc = xmlParser.parse(rss.text, { parseAttributeValue: true, ignoreAttributes: false })
+        const xmlDoc = xmlParser.parse(rss, { parseAttributeValue: true, ignoreAttributes: false })
 
         const enclosure = xmlDoc.rss.channel.item[0].enclosure
         expect(enclosure).to.exist
@@ -153,8 +163,8 @@ describe('Test syndication feeds', () => {
 
     it('Should contain a valid \'attachments\' object (covers JSON feed 1.0 endpoint)', async function () {
       for (const server of servers) {
-        const json = await getJSONfeed(server.url, 'videos')
-        const jsonObj = JSON.parse(json.text)
+        const json = await server.feed.getJSON({ feed: 'videos' })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(2)
         expect(jsonObj.items[0].attachments).to.exist
         expect(jsonObj.items[0].attachments.length).to.be.eq(1)
@@ -166,16 +176,16 @@ describe('Test syndication feeds', () => {
 
     it('Should filter by account', async function () {
       {
-        const json = await getJSONfeed(servers[0].url, 'videos', { accountId: rootAccountId })
-        const jsonObj = JSON.parse(json.text)
+        const json = await servers[0].feed.getJSON({ feed: 'videos', query: { accountId: rootAccountId } })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(1)
         expect(jsonObj.items[0].title).to.equal('my super name for server 1')
         expect(jsonObj.items[0].author.name).to.equal('root')
       }
 
       {
-        const json = await getJSONfeed(servers[0].url, 'videos', { accountId: userAccountId })
-        const jsonObj = JSON.parse(json.text)
+        const json = await servers[0].feed.getJSON({ feed: 'videos', query: { accountId: userAccountId } })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(1)
         expect(jsonObj.items[0].title).to.equal('user video')
         expect(jsonObj.items[0].author.name).to.equal('john')
@@ -183,15 +193,15 @@ describe('Test syndication feeds', () => {
 
       for (const server of servers) {
         {
-          const json = await getJSONfeed(server.url, 'videos', { accountName: 'root@localhost:' + servers[0].port })
-          const jsonObj = JSON.parse(json.text)
+          const json = await server.feed.getJSON({ feed: 'videos', query: { accountName: 'root@localhost:' + servers[0].port } })
+          const jsonObj = JSON.parse(json)
           expect(jsonObj.items.length).to.be.equal(1)
           expect(jsonObj.items[0].title).to.equal('my super name for server 1')
         }
 
         {
-          const json = await getJSONfeed(server.url, 'videos', { accountName: 'john@localhost:' + servers[0].port })
-          const jsonObj = JSON.parse(json.text)
+          const json = await server.feed.getJSON({ feed: 'videos', query: { accountName: 'john@localhost:' + servers[0].port } })
+          const jsonObj = JSON.parse(json)
           expect(jsonObj.items.length).to.be.equal(1)
           expect(jsonObj.items[0].title).to.equal('user video')
         }
@@ -200,16 +210,16 @@ describe('Test syndication feeds', () => {
 
     it('Should filter by video channel', async function () {
       {
-        const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelId: rootChannelId })
-        const jsonObj = JSON.parse(json.text)
+        const json = await servers[0].feed.getJSON({ feed: 'videos', query: { videoChannelId: rootChannelId } })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(1)
         expect(jsonObj.items[0].title).to.equal('my super name for server 1')
         expect(jsonObj.items[0].author.name).to.equal('root')
       }
 
       {
-        const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelId: userChannelId })
-        const jsonObj = JSON.parse(json.text)
+        const json = await servers[0].feed.getJSON({ feed: 'videos', query: { videoChannelId: userChannelId } })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(1)
         expect(jsonObj.items[0].title).to.equal('user video')
         expect(jsonObj.items[0].author.name).to.equal('john')
@@ -217,15 +227,17 @@ describe('Test syndication feeds', () => {
 
       for (const server of servers) {
         {
-          const json = await getJSONfeed(server.url, 'videos', { videoChannelName: 'root_channel@localhost:' + servers[0].port })
-          const jsonObj = JSON.parse(json.text)
+          const query = { videoChannelName: 'root_channel@localhost:' + servers[0].port }
+          const json = await server.feed.getJSON({ feed: 'videos', query })
+          const jsonObj = JSON.parse(json)
           expect(jsonObj.items.length).to.be.equal(1)
           expect(jsonObj.items[0].title).to.equal('my super name for server 1')
         }
 
         {
-          const json = await getJSONfeed(server.url, 'videos', { videoChannelName: 'john_channel@localhost:' + servers[0].port })
-          const jsonObj = JSON.parse(json.text)
+          const query = { videoChannelName: 'john_channel@localhost:' + servers[0].port }
+          const json = await server.feed.getJSON({ feed: 'videos', query })
+          const jsonObj = JSON.parse(json)
           expect(jsonObj.items.length).to.be.equal(1)
           expect(jsonObj.items[0].title).to.equal('user video')
         }
@@ -235,12 +247,12 @@ describe('Test syndication feeds', () => {
     it('Should correctly have videos feed with HLS only', async function () {
       this.timeout(120000)
 
-      await uploadVideo(serverHLSOnly.url, serverHLSOnly.accessToken, { name: 'hls only video' })
+      await serverHLSOnly.videos.upload({ attributes: { name: 'hls only video' } })
 
       await waitJobs([ serverHLSOnly ])
 
-      const json = await getJSONfeed(serverHLSOnly.url, 'videos')
-      const jsonObj = JSON.parse(json.text)
+      const json = await serverHLSOnly.feed.getJSON({ feed: 'videos' })
+      const jsonObj = JSON.parse(json)
       expect(jsonObj.items.length).to.be.equal(1)
       expect(jsonObj.items[0].attachments).to.exist
       expect(jsonObj.items[0].attachments.length).to.be.eq(4)
@@ -257,9 +269,9 @@ describe('Test syndication feeds', () => {
 
     it('Should contain valid comments (covers JSON feed 1.0 endpoint) and not from unlisted videos', async function () {
       for (const server of servers) {
-        const json = await getJSONfeed(server.url, 'video-comments')
+        const json = await server.feed.getJSON({ feed: 'video-comments' })
 
-        const jsonObj = JSON.parse(json.text)
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(2)
         expect(jsonObj.items[0].html_content).to.equal('super comment 2')
         expect(jsonObj.items[1].html_content).to.equal('super comment 1')
@@ -271,32 +283,32 @@ describe('Test syndication feeds', () => {
 
       const remoteHandle = 'root@localhost:' + servers[0].port
 
-      await addAccountToServerBlocklist(servers[1].url, servers[1].accessToken, remoteHandle)
+      await servers[1].blocklist.addToServerBlocklist({ account: remoteHandle })
 
       {
-        const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 2 })
-        const jsonObj = JSON.parse(json.text)
+        const json = await servers[1].feed.getJSON({ feed: 'video-comments', query: { version: 2 } })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(0)
       }
 
-      await removeAccountFromServerBlocklist(servers[1].url, servers[1].accessToken, remoteHandle)
+      await servers[1].blocklist.removeFromServerBlocklist({ account: remoteHandle })
 
       {
-        const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'server 2' })).uuid
+        const videoUUID = (await servers[1].videos.quickUpload({ name: 'server 2' })).uuid
         await waitJobs(servers)
-        await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'super comment')
+        await servers[0].comments.createThread({ videoId: videoUUID, text: 'super comment' })
         await waitJobs(servers)
 
-        const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 3 })
-        const jsonObj = JSON.parse(json.text)
+        const json = await servers[1].feed.getJSON({ feed: 'video-comments', query: { version: 3 } })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(3)
       }
 
-      await addAccountToAccountBlocklist(servers[1].url, servers[1].accessToken, remoteHandle)
+      await servers[1].blocklist.addToMyBlocklist({ account: remoteHandle })
 
       {
-        const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 4 })
-        const jsonObj = JSON.parse(json.text)
+        const json = await servers[1].feed.getJSON({ feed: 'video-comments', query: { version: 4 } })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(2)
       }
     })
@@ -308,66 +320,64 @@ describe('Test syndication feeds', () => {
 
     it('Should list no videos for a user with no videos and no subscriptions', async function () {
       const attr = { username: 'feeduser', password: 'password' }
-      await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: attr.username, password: attr.password })
-      const feeduserAccessToken = await userLogin(servers[0], attr)
+      await servers[0].users.create({ username: attr.username, password: attr.password })
+      const feeduserAccessToken = await servers[0].login.getAccessToken(attr)
 
       {
-        const res = await getMyUserInformation(servers[0].url, feeduserAccessToken)
-        const user: User = res.body
+        const user = await servers[0].users.getMyInfo({ token: feeduserAccessToken })
         feeduserAccountId = user.account.id
       }
 
       {
-        const res = await getUserScopedTokens(servers[0].url, feeduserAccessToken)
-        const token: ScopedToken = res.body
+        const token = await servers[0].users.getMyScopedTokens({ token: feeduserAccessToken })
         feeduserFeedToken = token.feedToken
       }
 
       {
-        const res = await listUserSubscriptionVideos(servers[0].url, feeduserAccessToken)
-        expect(res.body.total).to.equal(0)
+        const body = await servers[0].subscriptions.listVideos({ token: feeduserAccessToken })
+        expect(body.total).to.equal(0)
 
-        const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: feeduserAccountId, token: feeduserFeedToken })
-        const jsonObj = JSON.parse(json.text)
+        const query = { accountId: feeduserAccountId, token: feeduserFeedToken }
+        const json = await servers[0].feed.getJSON({ feed: 'subscriptions', query })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(0) // no subscription, it should not list the instance's videos but list 0 videos
       }
     })
 
     it('Should fail with an invalid token', async function () {
-      await getJSONfeed(servers[0].url, 'subscriptions', { accountId: feeduserAccountId, token: 'toto' }, HttpStatusCode.FORBIDDEN_403)
+      const query = { accountId: feeduserAccountId, token: 'toto' }
+      await servers[0].feed.getJSON({ feed: 'subscriptions', query, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should fail with a token of another user', async function () {
-      await getJSONfeed(
-        servers[0].url,
-        'subscriptions',
-        { accountId: feeduserAccountId, token: userFeedToken },
-        HttpStatusCode.FORBIDDEN_403
-      )
+      const query = { accountId: feeduserAccountId, token: userFeedToken }
+      await servers[0].feed.getJSON({ feed: 'subscriptions', query, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should list no videos for a user with videos but no subscriptions', async function () {
-      const res = await listUserSubscriptionVideos(servers[0].url, userAccessToken)
-      expect(res.body.total).to.equal(0)
+      const body = await servers[0].subscriptions.listVideos({ token: userAccessToken })
+      expect(body.total).to.equal(0)
 
-      const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken })
-      const jsonObj = JSON.parse(json.text)
+      const query = { accountId: userAccountId, token: userFeedToken }
+      const json = await servers[0].feed.getJSON({ feed: 'subscriptions', query })
+      const jsonObj = JSON.parse(json)
       expect(jsonObj.items.length).to.be.equal(0) // no subscription, it should not list the instance's videos but list 0 videos
     })
 
     it('Should list self videos for a user with a subscription to themselves', async function () {
       this.timeout(30000)
 
-      await addUserSubscription(servers[0].url, userAccessToken, 'john_channel@localhost:' + servers[0].port)
+      await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'john_channel@localhost:' + servers[0].port })
       await waitJobs(servers)
 
       {
-        const res = await listUserSubscriptionVideos(servers[0].url, userAccessToken)
-        expect(res.body.total).to.equal(1)
-        expect(res.body.data[0].name).to.equal('user video')
+        const body = await servers[0].subscriptions.listVideos({ token: userAccessToken })
+        expect(body.total).to.equal(1)
+        expect(body.data[0].name).to.equal('user video')
 
-        const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken, version: 1 })
-        const jsonObj = JSON.parse(json.text)
+        const query = { accountId: userAccountId, token: userFeedToken, version: 1 }
+        const json = await servers[0].feed.getJSON({ feed: 'subscriptions', query })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(1) // subscribed to self, it should not list the instance's videos but list john's
       }
     })
@@ -375,36 +385,33 @@ describe('Test syndication feeds', () => {
     it('Should list videos of a user\'s subscription', async function () {
       this.timeout(30000)
 
-      await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:' + servers[0].port)
+      await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@localhost:' + servers[0].port })
       await waitJobs(servers)
 
       {
-        const res = await listUserSubscriptionVideos(servers[0].url, userAccessToken)
-        expect(res.body.total).to.equal(2, "there should be 2 videos part of the subscription")
+        const body = await servers[0].subscriptions.listVideos({ token: userAccessToken })
+        expect(body.total).to.equal(2, "there should be 2 videos part of the subscription")
 
-        const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken, version: 2 })
-        const jsonObj = JSON.parse(json.text)
+        const query = { accountId: userAccountId, token: userFeedToken, version: 2 }
+        const json = await servers[0].feed.getJSON({ feed: 'subscriptions', query })
+        const jsonObj = JSON.parse(json)
         expect(jsonObj.items.length).to.be.equal(2) // subscribed to root, it should not list the instance's videos but list root/john's
       }
     })
 
     it('Should renew the token, and so have an invalid old token', async function () {
-      await renewUserScopedTokens(servers[0].url, userAccessToken)
-
-      await getJSONfeed(
-        servers[0].url,
-        'subscriptions',
-        { accountId: userAccountId, token: userFeedToken, version: 3 },
-        HttpStatusCode.FORBIDDEN_403
-      )
+      await servers[0].users.renewMyScopedTokens({ token: userAccessToken })
+
+      const query = { accountId: userAccountId, token: userFeedToken, version: 3 }
+      await servers[0].feed.getJSON({ feed: 'subscriptions', query, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     })
 
     it('Should succeed with the new token', async function () {
-      const res2 = await getUserScopedTokens(servers[0].url, userAccessToken)
-      const token: ScopedToken = res2.body
+      const token = await servers[0].users.getMyScopedTokens({ token: userAccessToken })
       userFeedToken = token.feedToken
 
-      await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken, version: 4 })
+      const query = { accountId: userAccountId, token: userFeedToken, version: 4 }
+      await servers[0].feed.getJSON({ feed: 'subscriptions', query })
     })
 
   })
index 59b1369471b2c86e31a0a6612fa6a8c703b07e83..c4ae777f55f0b4426a29107ed79b00b89df49e44 100644 (file)
@@ -18,12 +18,12 @@ async function register ({ transcodingManager }) {
       const builder = (options) => {
         return {
           outputOptions: [
-            '-r:' + options.streamNum + ' 5'
+            '-r:' + options.streamNum + ' 50'
           ]
         }
       }
 
-      transcodingManager.addLiveProfile('libx264', 'low-live', builder)
+      transcodingManager.addLiveProfile('libx264', 'high-live', builder)
     }
   }
 
@@ -45,7 +45,7 @@ async function register ({ transcodingManager }) {
       const builder = () => {
         return {
           inputOptions: [
-            '-r 5'
+            '-r 50'
           ]
         }
       }
@@ -82,7 +82,6 @@ async function register ({ transcodingManager }) {
   }
 }
 
-
 async function unregister () {
   return
 }
index 3e650e0a12c542588781695af6f98919ecf7dcea..06527bd358fbd2a28561df99011fe64da9db6cf7 100644 (file)
@@ -1,46 +1,46 @@
 async function register ({
-  registerHook,
-  registerSetting,
-  settingsManager,
-  storageManager,
   videoCategoryManager,
   videoLicenceManager,
   videoLanguageManager,
   videoPrivacyManager,
-  playlistPrivacyManager
+  playlistPrivacyManager,
+  getRouter
 }) {
-  videoLanguageManager.addLanguage('al_bhed', 'Al Bhed')
+  videoLanguageManager.addConstant('al_bhed', 'Al Bhed')
   videoLanguageManager.addLanguage('al_bhed2', 'Al Bhed 2')
-  videoLanguageManager.addLanguage('al_bhed3', 'Al Bhed 3')
-  videoLanguageManager.deleteLanguage('en')
+  videoLanguageManager.addConstant('al_bhed3', 'Al Bhed 3')
+  videoLanguageManager.deleteConstant('en')
   videoLanguageManager.deleteLanguage('fr')
-  videoLanguageManager.deleteLanguage('al_bhed3')
+  videoLanguageManager.deleteConstant('al_bhed3')
 
   videoCategoryManager.addCategory(42, 'Best category')
-  videoCategoryManager.addCategory(43, 'High best category')
-  videoCategoryManager.deleteCategory(1) // Music
+  videoCategoryManager.addConstant(43, 'High best category')
+  videoCategoryManager.deleteConstant(1) // Music
   videoCategoryManager.deleteCategory(2) // Films
 
   videoLicenceManager.addLicence(42, 'Best licence')
-  videoLicenceManager.addLicence(43, 'High best licence')
-  videoLicenceManager.deleteLicence(1) // Attribution
-  videoLicenceManager.deleteLicence(7) // Public domain
+  videoLicenceManager.addConstant(43, 'High best licence')
+  videoLicenceManager.deleteConstant(1) // Attribution
+  videoLicenceManager.deleteConstant(7) // Public domain
 
+  videoPrivacyManager.deleteConstant(2)
   videoPrivacyManager.deletePrivacy(2)
+  playlistPrivacyManager.deleteConstant(3)
   playlistPrivacyManager.deletePlaylistPrivacy(3)
-}
 
-async function unregister () {
-  return
+  {
+    const router = getRouter()
+    router.get('/reset-categories', (req, res) => {
+      videoCategoryManager.resetConstants()
+
+      res.sendStatus(204)
+    })
+  }
 }
 
+async function unregister () {}
+
 module.exports = {
   register,
   unregister
 }
-
-// ############################################################################
-
-function addToCount (obj) {
-  return Object.assign({}, obj, { count: obj.count + 1 })
-}
index f8e6f0b98129406ce82742b57abd0fc684afb2ff..db405ff31e16b01c6380c17935c7fd5510aaf7f7 100644 (file)
@@ -234,7 +234,7 @@ async function register ({ registerHook, registerSetting, settingsManager, stora
   })
 
   {
-    const searchHooks = [
+    const filterHooks = [
       'filter:api.search.videos.local.list.params',
       'filter:api.search.videos.local.list.result',
       'filter:api.search.videos.index.list.params',
@@ -246,10 +246,13 @@ async function register ({ registerHook, registerSetting, settingsManager, stora
       'filter:api.search.video-playlists.local.list.params',
       'filter:api.search.video-playlists.local.list.result',
       'filter:api.search.video-playlists.index.list.params',
-      'filter:api.search.video-playlists.index.list.result'
+      'filter:api.search.video-playlists.index.list.result',
+
+      'filter:api.overviews.videos.list.params',
+      'filter:api.overviews.videos.list.result'
     ]
 
-    for (const h of searchHooks) {
+    for (const h of filterHooks) {
       registerHook({
         target: h,
         handler: (obj) => {
diff --git a/server/tests/fixtures/video_very_short_240p.mp4 b/server/tests/fixtures/video_very_short_240p.mp4
new file mode 100644 (file)
index 0000000..95b6be9
Binary files /dev/null and b/server/tests/fixtures/video_very_short_240p.mp4 differ
index 4c51b70003aa2f3805132ced1a769c0f7b9837ad..31dc6ec72f91ef41cef16aeff3ae1e5a289fd7fc 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import { VideoCommentModel } from '../../models/video/video-comment'
 
 const expect = chai.expect
index c028b316d9695512273705d92ad3a15afa9721ee..d5cac51a328e64bf38f39315410e5eafdf27ce00 100644 (file)
@@ -1,10 +1,10 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
+import * as chai from 'chai'
 import { snakeCase } from 'lodash'
-import { objectConverter, parseBytes } from '../../helpers/core-utils'
 import validator from 'validator'
+import { objectConverter, parseBytes } from '../../helpers/core-utils'
 
 const expect = chai.expect
 
index 54911697fe55437fcc6e2308c05c3bb5b35ef0cf..9fe9aa4cba8c07bc4fea281183dc97aacfafbef8 100644 (file)
@@ -1,11 +1,11 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
+import { expect } from 'chai'
 import { readFile, remove } from 'fs-extra'
 import { join } from 'path'
 import { processImage } from '../../../server/helpers/image-utils'
 import { buildAbsoluteFixturePath, root } from '../../../shared/extra-utils'
-import { expect } from 'chai'
 
 async function checkBuffers (path1: string, path2: string, equals: boolean) {
   const [ buf1, buf2 ] = await Promise.all([
index 5e77f129ea5b24bd7ed38c9375612a97ca773ac7..7f7873df3492039f44fae3ee9cee93fffc7b038d 100644 (file)
@@ -4,7 +4,7 @@ import 'mocha'
 import { expect } from 'chai'
 import { pathExists, remove } from 'fs-extra'
 import { join } from 'path'
-import { get4KFileUrl, root, wait } from '../../../shared/extra-utils'
+import { FIXTURE_URLS, root, wait } from '../../../shared/extra-utils'
 import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
 
 describe('Request helpers', function () {
@@ -13,7 +13,7 @@ describe('Request helpers', function () {
 
   it('Should throw an error when the bytes limit is exceeded for request', async function () {
     try {
-      await doRequest(get4KFileUrl(), { bodyKBLimit: 3 })
+      await doRequest(FIXTURE_URLS.video4K, { bodyKBLimit: 3 })
     } catch {
       return
     }
@@ -23,7 +23,7 @@ describe('Request helpers', function () {
 
   it('Should throw an error when the bytes limit is exceeded for request and save file', async function () {
     try {
-      await doRequestAndSaveToFile(get4KFileUrl(), destPath1, { bodyKBLimit: 3 })
+      await doRequestAndSaveToFile(FIXTURE_URLS.video4K, destPath1, { bodyKBLimit: 3 })
     } catch {
 
       await wait(500)
@@ -35,8 +35,8 @@ describe('Request helpers', function () {
   })
 
   it('Should succeed if the file is below the limit', async function () {
-    await doRequest(get4KFileUrl(), { bodyKBLimit: 5 })
-    await doRequestAndSaveToFile(get4KFileUrl(), destPath2, { bodyKBLimit: 5 })
+    await doRequest(FIXTURE_URLS.video4K, { bodyKBLimit: 5 })
+    await doRequestAndSaveToFile(FIXTURE_URLS.video4K, destPath2, { bodyKBLimit: 5 })
 
     expect(await pathExists(destPath2)).to.be.true
   })
index 3fbd0ebbde20b0fb0abd17047909307c0d4e93c6..1718ac424079da029bb2f92428ca1a7addb868ea 100644 (file)
@@ -6,3 +6,4 @@ import './cli/'
 import './api/'
 import './plugins/'
 import './helpers/'
+import './lib/'
diff --git a/server/tests/lib/index.ts b/server/tests/lib/index.ts
new file mode 100644 (file)
index 0000000..a40df35
--- /dev/null
@@ -0,0 +1 @@
+export * from './video-constant-registry-factory'
diff --git a/server/tests/lib/video-constant-registry-factory.ts b/server/tests/lib/video-constant-registry-factory.ts
new file mode 100644 (file)
index 0000000..e26b286
--- /dev/null
@@ -0,0 +1,155 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions */
+import 'mocha'
+import { expect } from 'chai'
+import { VideoConstantManagerFactory } from '@server/lib/plugins/video-constant-manager-factory'
+import {
+  VIDEO_CATEGORIES,
+  VIDEO_LANGUAGES,
+  VIDEO_LICENCES,
+  VIDEO_PLAYLIST_PRIVACIES,
+  VIDEO_PRIVACIES
+} from '@server/initializers/constants'
+import {
+  VideoPlaylistPrivacy,
+  VideoPrivacy
+} from '@shared/models'
+
+describe('VideoConstantManagerFactory', function () {
+  const factory = new VideoConstantManagerFactory('peertube-plugin-constants')
+
+  afterEach(() => {
+    factory.resetVideoConstants('peertube-plugin-constants')
+  })
+
+  describe('VideoCategoryManager', () => {
+    const videoCategoryManager = factory.createVideoConstantManager<number>('category')
+
+    it('Should be able to list all video category constants', () => {
+      const constants = videoCategoryManager.getConstants()
+      expect(constants).to.deep.equal(VIDEO_CATEGORIES)
+    })
+
+    it('Should be able to delete a video category constant', () => {
+      const successfullyDeleted = videoCategoryManager.deleteConstant(1)
+      expect(successfullyDeleted).to.be.true
+      expect(videoCategoryManager.getConstantValue(1)).to.be.undefined
+    })
+
+    it('Should be able to add a video category constant', () => {
+      const successfullyAdded = videoCategoryManager.addConstant(42, 'The meaning of life')
+      expect(successfullyAdded).to.be.true
+      expect(videoCategoryManager.getConstantValue(42)).to.equal('The meaning of life')
+    })
+
+    it('Should be able to reset video category constants', () => {
+      videoCategoryManager.deleteConstant(1)
+      videoCategoryManager.resetConstants()
+      expect(videoCategoryManager.getConstantValue(1)).not.be.undefined
+    })
+  })
+
+  describe('VideoLicenceManager', () => {
+    const videoLicenceManager = factory.createVideoConstantManager<number>('licence')
+    it('Should be able to list all video licence constants', () => {
+      const constants = videoLicenceManager.getConstants()
+      expect(constants).to.deep.equal(VIDEO_LICENCES)
+    })
+
+    it('Should be able to delete a video licence constant', () => {
+      const successfullyDeleted = videoLicenceManager.deleteConstant(1)
+      expect(successfullyDeleted).to.be.true
+      expect(videoLicenceManager.getConstantValue(1)).to.be.undefined
+    })
+
+    it('Should be able to add a video licence constant', () => {
+      const successfullyAdded = videoLicenceManager.addConstant(42, 'European Union Public Licence')
+      expect(successfullyAdded).to.be.true
+      expect(videoLicenceManager.getConstantValue(42)).to.equal('European Union Public Licence')
+    })
+
+    it('Should be able to reset video licence constants', () => {
+      videoLicenceManager.deleteConstant(1)
+      videoLicenceManager.resetConstants()
+      expect(videoLicenceManager.getConstantValue(1)).not.be.undefined
+    })
+  })
+
+  describe('PlaylistPrivacyManager', () => {
+    const playlistPrivacyManager = factory.createVideoConstantManager<VideoPlaylistPrivacy>('playlistPrivacy')
+    it('Should be able to list all video playlist privacy constants', () => {
+      const constants = playlistPrivacyManager.getConstants()
+      expect(constants).to.deep.equal(VIDEO_PLAYLIST_PRIVACIES)
+    })
+
+    it('Should be able to delete a video playlist privacy constant', () => {
+      const successfullyDeleted = playlistPrivacyManager.deleteConstant(1)
+      expect(successfullyDeleted).to.be.true
+      expect(playlistPrivacyManager.getConstantValue(1)).to.be.undefined
+    })
+
+    it('Should be able to add a video playlist privacy constant', () => {
+      const successfullyAdded = playlistPrivacyManager.addConstant(42, 'Friends only')
+      expect(successfullyAdded).to.be.true
+      expect(playlistPrivacyManager.getConstantValue(42)).to.equal('Friends only')
+    })
+
+    it('Should be able to reset video playlist privacy constants', () => {
+      playlistPrivacyManager.deleteConstant(1)
+      playlistPrivacyManager.resetConstants()
+      expect(playlistPrivacyManager.getConstantValue(1)).not.be.undefined
+    })
+  })
+
+  describe('VideoPrivacyManager', () => {
+    const videoPrivacyManager = factory.createVideoConstantManager<VideoPrivacy>('privacy')
+    it('Should be able to list all video privacy constants', () => {
+      const constants = videoPrivacyManager.getConstants()
+      expect(constants).to.deep.equal(VIDEO_PRIVACIES)
+    })
+
+    it('Should be able to delete a video privacy constant', () => {
+      const successfullyDeleted = videoPrivacyManager.deleteConstant(1)
+      expect(successfullyDeleted).to.be.true
+      expect(videoPrivacyManager.getConstantValue(1)).to.be.undefined
+    })
+
+    it('Should be able to add a video privacy constant', () => {
+      const successfullyAdded = videoPrivacyManager.addConstant(42, 'Friends only')
+      expect(successfullyAdded).to.be.true
+      expect(videoPrivacyManager.getConstantValue(42)).to.equal('Friends only')
+    })
+
+    it('Should be able to reset video privacy constants', () => {
+      videoPrivacyManager.deleteConstant(1)
+      videoPrivacyManager.resetConstants()
+      expect(videoPrivacyManager.getConstantValue(1)).not.be.undefined
+    })
+  })
+
+  describe('VideoLanguageManager', () => {
+    const videoLanguageManager = factory.createVideoConstantManager<string>('language')
+    it('Should be able to list all video language constants', () => {
+      const constants = videoLanguageManager.getConstants()
+      expect(constants).to.deep.equal(VIDEO_LANGUAGES)
+    })
+
+    it('Should be able to add a video language constant', () => {
+      const successfullyAdded = videoLanguageManager.addConstant('fr', 'Fr occitan')
+      expect(successfullyAdded).to.be.true
+      expect(videoLanguageManager.getConstantValue('fr')).to.equal('Fr occitan')
+    })
+
+    it('Should be able to delete a video language constant', () => {
+      videoLanguageManager.addConstant('fr', 'Fr occitan')
+      const successfullyDeleted = videoLanguageManager.deleteConstant('fr')
+      expect(successfullyDeleted).to.be.true
+      expect(videoLanguageManager.getConstantValue('fr')).to.be.undefined
+    })
+
+    it('Should be able to reset video language constants', () => {
+      videoLanguageManager.addConstant('fr', 'Fr occitan')
+      videoLanguageManager.resetConstants()
+      expect(videoLanguageManager.getConstantValue('fr')).to.be.undefined
+    })
+  })
+})
index 09e5afcf9b4bc6c9fc58eb3032638f3798d520da..4968eef089c8487ccb3a68cae8e5f3a6fd0069f2 100644 (file)
@@ -2,28 +2,18 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import {
-  addVideoChannel,
-  cleanupTests,
-  createUser,
-  flushAndRunServer,
-  makeGetRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  uploadVideo
-} from '../../shared/extra-utils'
-import { VideoPrivacy } from '../../shared/models/videos'
-import { HttpStatusCode } from '@shared/core-utils'
+import { cleanupTests, createSingleServer, makeGetRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/extra-utils'
+import { HttpStatusCode, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test misc endpoints', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   before(async function () {
     this.timeout(120000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
   })
 
@@ -33,7 +23,7 @@ describe('Test misc endpoints', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/.well-known/security.txt',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.text).to.contain('security issue')
@@ -43,7 +33,7 @@ describe('Test misc endpoints', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/.well-known/nodeinfo',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body.links).to.be.an('array')
@@ -55,7 +45,7 @@ describe('Test misc endpoints', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/.well-known/dnt-policy.txt',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.text).to.contain('http://www.w3.org/TR/tracking-dnt')
@@ -65,7 +55,7 @@ describe('Test misc endpoints', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/.well-known/dnt',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body.tracking).to.equal('N')
@@ -75,7 +65,7 @@ describe('Test misc endpoints', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/.well-known/change-password',
-        statusCodeExpected: HttpStatusCode.FOUND_302
+        expectedStatus: HttpStatusCode.FOUND_302
       })
 
       expect(res.header.location).to.equal('/my-account/settings')
@@ -88,7 +78,7 @@ describe('Test misc endpoints', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/.well-known/webfinger?resource=' + resource,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       const data = res.body
@@ -113,7 +103,7 @@ describe('Test misc endpoints', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/robots.txt',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.text).to.contain('User-agent')
@@ -123,7 +113,7 @@ describe('Test misc endpoints', function () {
       await makeGetRequest({
         url: server.url,
         path: '/security.txt',
-        statusCodeExpected: HttpStatusCode.MOVED_PERMANENTLY_301
+        expectedStatus: HttpStatusCode.MOVED_PERMANENTLY_301
       })
     })
 
@@ -131,7 +121,7 @@ describe('Test misc endpoints', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/nodeinfo/2.0.json',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body.software.name).to.equal('peertube')
@@ -146,7 +136,7 @@ describe('Test misc endpoints', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/sitemap.xml',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
@@ -157,7 +147,7 @@ describe('Test misc endpoints', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: '/sitemap.xml',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
@@ -167,20 +157,20 @@ describe('Test misc endpoints', function () {
     it('Should add videos, channel and accounts and get sitemap', async function () {
       this.timeout(35000)
 
-      await uploadVideo(server.url, server.accessToken, { name: 'video 1', nsfw: false })
-      await uploadVideo(server.url, server.accessToken, { name: 'video 2', nsfw: false })
-      await uploadVideo(server.url, server.accessToken, { name: 'video 3', privacy: VideoPrivacy.PRIVATE })
+      await server.videos.upload({ attributes: { name: 'video 1', nsfw: false } })
+      await server.videos.upload({ attributes: { name: 'video 2', nsfw: false } })
+      await server.videos.upload({ attributes: { name: 'video 3', privacy: VideoPrivacy.PRIVATE } })
 
-      await addVideoChannel(server.url, server.accessToken, { name: 'channel1', displayName: 'channel 1' })
-      await addVideoChannel(server.url, server.accessToken, { name: 'channel2', displayName: 'channel 2' })
+      await server.channels.create({ attributes: { name: 'channel1', displayName: 'channel 1' } })
+      await server.channels.create({ attributes: { name: 'channel2', displayName: 'channel 2' } })
 
-      await createUser({ url: server.url, accessToken: server.accessToken, username: 'user1', password: 'password' })
-      await createUser({ url: server.url, accessToken: server.accessToken, username: 'user2', password: 'password' })
+      await server.users.create({ username: 'user1', password: 'password' })
+      await server.users.create({ username: 'user2', password: 'password' })
 
       const res = await makeGetRequest({
         url: server.url,
         path: '/sitemap.xml?t=1', // avoid using cache
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
index 0f57ef7fee486876d991b74e239c165f71f605cd..4c1bc7d06382dc555f0ee9042c52d7df3d07d46c 100644 (file)
@@ -1,63 +1,38 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
-import {
-  addVideoCommentReply,
-  addVideoCommentThread,
-  addVideoInPlaylist,
-  blockUser,
-  createLive,
-  createUser,
-  createVideoPlaylist,
-  deleteVideoComment,
-  getPluginTestPath,
-  installPlugin,
-  registerUser,
-  removeUser,
-  setAccessTokensToServers,
-  setDefaultVideoChannel,
-  unblockUser,
-  updateUser,
-  updateVideo,
-  uploadVideo,
-  userLogin,
-  viewVideo
-} from '../../../shared/extra-utils'
 import {
   cleanupTests,
-  flushAndRunMultipleServers,
+  createMultipleServers,
   killallServers,
-  reRunServer,
-  ServerInfo,
-  waitUntilLog
-} from '../../../shared/extra-utils/server/servers'
+  PeerTubeServer,
+  PluginsCommand,
+  setAccessTokensToServers,
+  setDefaultVideoChannel
+} from '@shared/extra-utils'
+import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
 
 describe('Test plugin action hooks', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let videoUUID: string
   let threadId: number
 
   function checkHook (hook: ServerHookName) {
-    return waitUntilLog(servers[0], 'Run hook ' + hook)
+    return servers[0].servers.waitUntilLog('Run hook ' + hook)
   }
 
   before(async function () {
     this.timeout(30000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
 
-    await installPlugin({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      path: getPluginTestPath()
-    })
+    await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath() })
 
-    killallServers([ servers[0] ])
+    await killallServers([ servers[0] ])
 
-    await reRunServer(servers[0], {
+    await servers[0].run({
       live: {
         enabled: true
       }
@@ -73,20 +48,20 @@ describe('Test plugin action hooks', function () {
   describe('Videos hooks', function () {
 
     it('Should run action:api.video.uploaded', async function () {
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' })
-      videoUUID = res.body.video.uuid
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video' } })
+      videoUUID = uuid
 
       await checkHook('action:api.video.uploaded')
     })
 
     it('Should run action:api.video.updated', async function () {
-      await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video updated' })
+      await servers[0].videos.update({ id: videoUUID, attributes: { name: 'video updated' } })
 
       await checkHook('action:api.video.updated')
     })
 
     it('Should run action:api.video.viewed', async function () {
-      await viewVideo(servers[0].url, videoUUID)
+      await servers[0].videos.view({ id: videoUUID })
 
       await checkHook('action:api.video.viewed')
     })
@@ -98,10 +73,10 @@ describe('Test plugin action hooks', function () {
       const attributes = {
         name: 'live',
         privacy: VideoPrivacy.PUBLIC,
-        channelId: servers[0].videoChannel.id
+        channelId: servers[0].store.channel.id
       }
 
-      await createLive(servers[0].url, servers[0].accessToken, attributes)
+      await servers[0].live.create({ fields: attributes })
 
       await checkHook('action:api.live-video.created')
     })
@@ -109,20 +84,20 @@ describe('Test plugin action hooks', function () {
 
   describe('Comments hooks', function () {
     it('Should run action:api.video-thread.created', async function () {
-      const res = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'thread')
-      threadId = res.body.comment.id
+      const created = await servers[0].comments.createThread({ videoId: videoUUID, text: 'thread' })
+      threadId = created.id
 
       await checkHook('action:api.video-thread.created')
     })
 
     it('Should run action:api.video-comment-reply.created', async function () {
-      await addVideoCommentReply(servers[0].url, servers[0].accessToken, videoUUID, threadId, 'reply')
+      await servers[0].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text: 'reply' })
 
       await checkHook('action:api.video-comment-reply.created')
     })
 
     it('Should run action:api.video-comment.deleted', async function () {
-      await deleteVideoComment(servers[0].url, servers[0].accessToken, videoUUID, threadId)
+      await servers[0].comments.delete({ videoId: videoUUID, commentId: threadId })
 
       await checkHook('action:api.video-comment.deleted')
     })
@@ -132,49 +107,44 @@ describe('Test plugin action hooks', function () {
     let userId: number
 
     it('Should run action:api.user.registered', async function () {
-      await registerUser(servers[0].url, 'registered_user', 'super_password')
+      await servers[0].users.register({ username: 'registered_user' })
 
       await checkHook('action:api.user.registered')
     })
 
     it('Should run action:api.user.created', async function () {
-      const res = await createUser({
-        url: servers[0].url,
-        accessToken: servers[0].accessToken,
-        username: 'created_user',
-        password: 'super_password'
-      })
-      userId = res.body.user.id
+      const user = await servers[0].users.create({ username: 'created_user' })
+      userId = user.id
 
       await checkHook('action:api.user.created')
     })
 
     it('Should run action:api.user.oauth2-got-token', async function () {
-      await userLogin(servers[0], { username: 'created_user', password: 'super_password' })
+      await servers[0].login.login({ user: { username: 'created_user' } })
 
       await checkHook('action:api.user.oauth2-got-token')
     })
 
     it('Should run action:api.user.blocked', async function () {
-      await blockUser(servers[0].url, userId, servers[0].accessToken)
+      await servers[0].users.banUser({ userId })
 
       await checkHook('action:api.user.blocked')
     })
 
     it('Should run action:api.user.unblocked', async function () {
-      await unblockUser(servers[0].url, userId, servers[0].accessToken)
+      await servers[0].users.unbanUser({ userId })
 
       await checkHook('action:api.user.unblocked')
     })
 
     it('Should run action:api.user.updated', async function () {
-      await updateUser({ url: servers[0].url, accessToken: servers[0].accessToken, userId, videoQuota: 50 })
+      await servers[0].users.update({ userId, videoQuota: 50 })
 
       await checkHook('action:api.user.updated')
     })
 
     it('Should run action:api.user.deleted', async function () {
-      await removeUser(servers[0].url, userId, servers[0].accessToken)
+      await servers[0].users.remove({ userId })
 
       await checkHook('action:api.user.deleted')
     })
@@ -186,30 +156,23 @@ describe('Test plugin action hooks', function () {
 
     before(async function () {
       {
-        const res = await createVideoPlaylist({
-          url: servers[0].url,
-          token: servers[0].accessToken,
-          playlistAttrs: {
+        const { id } = await servers[0].playlists.create({
+          attributes: {
             displayName: 'My playlist',
             privacy: VideoPlaylistPrivacy.PRIVATE
           }
         })
-        playlistId = res.body.videoPlaylist.id
+        playlistId = id
       }
 
       {
-        const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'my super name' })
-        videoId = res.body.video.id
+        const { id } = await servers[0].videos.upload({ attributes: { name: 'my super name' } })
+        videoId = id
       }
     })
 
     it('Should run action:api.video-playlist-element.created', async function () {
-      await addVideoInPlaylist({
-        url: servers[0].url,
-        token: servers[0].accessToken,
-        playlistId,
-        elementAttrs: { videoId }
-      })
+      await servers[0].playlists.addElement({ playlistId, attributes: { videoId } })
 
       await checkHook('action:api.video-playlist-element.created')
     })
index 5addb45c7826be08d5c2b2cf34556792ab481c6b..f3e018d437bfe8efbd861f93f6d68ebabd5bb541 100644 (file)
@@ -2,44 +2,32 @@
 
 import 'mocha'
 import { expect } from 'chai'
-import { ServerConfig, User, UserRole } from '@shared/models'
 import {
+  cleanupTests,
+  createSingleServer,
   decodeQueryString,
-  getConfig,
-  getExternalAuth,
-  getMyUserInformation,
-  getPluginTestPath,
-  installPlugin,
-  loginUsingExternalToken,
-  logout,
-  refreshToken,
+  PeerTubeServer,
+  PluginsCommand,
   setAccessTokensToServers,
-  uninstallPlugin,
-  updateMyUser,
-  wait,
-  userLogin,
-  updatePluginSettings,
-  createUser
-} from '../../../shared/extra-utils'
-import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+  wait
+} from '@shared/extra-utils'
+import { HttpStatusCode, UserRole } from '@shared/models'
 
 async function loginExternal (options: {
-  server: ServerInfo
+  server: PeerTubeServer
   npmName: string
   authName: string
   username: string
   query?: any
-  statusCodeExpected?: HttpStatusCode
-  statusCodeExpectedStep2?: HttpStatusCode
+  expectedStatus?: HttpStatusCode
+  expectedStatusStep2?: HttpStatusCode
 }) {
-  const res = await getExternalAuth({
-    url: options.server.url,
+  const res = await options.server.plugins.getExternalAuth({
     npmName: options.npmName,
     npmVersion: '0.0.1',
     authName: options.authName,
     query: options.query,
-    statusCodeExpected: options.statusCodeExpected || HttpStatusCode.FOUND_302
+    expectedStatus: options.expectedStatus || HttpStatusCode.FOUND_302
   })
 
   if (res.status !== HttpStatusCode.FOUND_302) return
@@ -47,18 +35,17 @@ async function loginExternal (options: {
   const location = res.header.location
   const { externalAuthToken } = decodeQueryString(location)
 
-  const resLogin = await loginUsingExternalToken(
-    options.server,
-    options.username,
-    externalAuthToken as string,
-    options.statusCodeExpectedStep2
-  )
+  const resLogin = await options.server.login.loginUsingExternalToken({
+    username: options.username,
+    externalAuthToken: externalAuthToken as string,
+    expectedStatus: options.expectedStatusStep2
+  })
 
   return resLogin.body
 }
 
 describe('Test external auth plugins', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   let cyanAccessToken: string
   let cyanRefreshToken: string
@@ -71,22 +58,16 @@ describe('Test external auth plugins', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
     for (const suffix of [ 'one', 'two', 'three' ]) {
-      await installPlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        path: getPluginTestPath('-external-auth-' + suffix)
-      })
+      await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-external-auth-' + suffix) })
     }
   })
 
   it('Should display the correct configuration', async function () {
-    const res = await getConfig(server.url)
-
-    const config: ServerConfig = res.body
+    const config = await server.config.getConfig()
 
     const auths = config.plugin.registeredExternalAuths
     expect(auths).to.have.lengthOf(8)
@@ -98,15 +79,14 @@ describe('Test external auth plugins', function () {
   })
 
   it('Should redirect for a Cyan login', async function () {
-    const res = await getExternalAuth({
-      url: server.url,
+    const res = await server.plugins.getExternalAuth({
       npmName: 'test-external-auth-one',
       npmVersion: '0.0.1',
       authName: 'external-auth-1',
       query: {
         username: 'cyan'
       },
-      statusCodeExpected: HttpStatusCode.FOUND_302
+      expectedStatus: HttpStatusCode.FOUND_302
     })
 
     const location = res.header.location
@@ -121,13 +101,17 @@ describe('Test external auth plugins', function () {
   })
 
   it('Should reject auto external login with a missing or invalid token', async function () {
-    await loginUsingExternalToken(server, 'cyan', '', HttpStatusCode.BAD_REQUEST_400)
-    await loginUsingExternalToken(server, 'cyan', 'blabla', HttpStatusCode.BAD_REQUEST_400)
+    const command = server.login
+
+    await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should reject auto external login with a missing or invalid username', async function () {
-    await loginUsingExternalToken(server, '', externalAuthToken, HttpStatusCode.BAD_REQUEST_400)
-    await loginUsingExternalToken(server, '', externalAuthToken, HttpStatusCode.BAD_REQUEST_400)
+    const command = server.login
+
+    await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should reject auto external login with an expired token', async function () {
@@ -135,9 +119,13 @@ describe('Test external auth plugins', function () {
 
     await wait(5000)
 
-    await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400)
+    await server.login.loginUsingExternalToken({
+      username: 'cyan',
+      externalAuthToken,
+      expectedStatus: HttpStatusCode.BAD_REQUEST_400
+    })
 
-    await waitUntilLog(server, 'expired external auth token', 2)
+    await server.servers.waitUntilLog('expired external auth token', 2)
   })
 
   it('Should auto login Cyan, create the user and use the token', async function () {
@@ -157,9 +145,7 @@ describe('Test external auth plugins', function () {
     }
 
     {
-      const res = await getMyUserInformation(server.url, cyanAccessToken)
-
-      const body: User = res.body
+      const body = await server.users.getMyInfo({ token: cyanAccessToken })
       expect(body.username).to.equal('cyan')
       expect(body.account.displayName).to.equal('cyan')
       expect(body.email).to.equal('cyan@example.com')
@@ -181,9 +167,7 @@ describe('Test external auth plugins', function () {
     }
 
     {
-      const res = await getMyUserInformation(server.url, kefkaAccessToken)
-
-      const body: User = res.body
+      const body = await server.users.getMyInfo({ token: kefkaAccessToken })
       expect(body.username).to.equal('kefka')
       expect(body.account.displayName).to.equal('Kefka Palazzo')
       expect(body.email).to.equal('kefka@example.com')
@@ -193,43 +177,39 @@ describe('Test external auth plugins', function () {
 
   it('Should refresh Cyan token, but not Kefka token', async function () {
     {
-      const resRefresh = await refreshToken(server, cyanRefreshToken)
+      const resRefresh = await server.login.refreshToken({ refreshToken: cyanRefreshToken })
       cyanAccessToken = resRefresh.body.access_token
       cyanRefreshToken = resRefresh.body.refresh_token
 
-      const res = await getMyUserInformation(server.url, cyanAccessToken)
-      const user: User = res.body
-      expect(user.username).to.equal('cyan')
+      const body = await server.users.getMyInfo({ token: cyanAccessToken })
+      expect(body.username).to.equal('cyan')
     }
 
     {
-      await refreshToken(server, kefkaRefreshToken, HttpStatusCode.BAD_REQUEST_400)
+      await server.login.refreshToken({ refreshToken: kefkaRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     }
   })
 
   it('Should update Cyan profile', async function () {
-    await updateMyUser({
-      url: server.url,
-      accessToken: cyanAccessToken,
+    await server.users.updateMe({
+      token: cyanAccessToken,
       displayName: 'Cyan Garamonde',
       description: 'Retainer to the king of Doma'
     })
 
-    const res = await getMyUserInformation(server.url, cyanAccessToken)
-
-    const body: User = res.body
+    const body = await server.users.getMyInfo({ token: cyanAccessToken })
     expect(body.account.displayName).to.equal('Cyan Garamonde')
     expect(body.account.description).to.equal('Retainer to the king of Doma')
   })
 
   it('Should logout Cyan', async function () {
-    await logout(server.url, cyanAccessToken)
+    await server.login.logout({ token: cyanAccessToken })
   })
 
   it('Should have logged out Cyan', async function () {
-    await waitUntilLog(server, 'On logout cyan')
+    await server.servers.waitUntilLog('On logout cyan')
 
-    await getMyUserInformation(server.url, cyanAccessToken, HttpStatusCode.UNAUTHORIZED_401)
+    await server.users.getMyInfo({ token: cyanAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
   })
 
   it('Should login Cyan and keep the old existing profile', async function () {
@@ -247,9 +227,7 @@ describe('Test external auth plugins', function () {
       cyanAccessToken = res.access_token
     }
 
-    const res = await getMyUserInformation(server.url, cyanAccessToken)
-
-    const body: User = res.body
+    const body = await server.users.getMyInfo({ token: cyanAccessToken })
     expect(body.username).to.equal('cyan')
     expect(body.account.displayName).to.equal('Cyan Garamonde')
     expect(body.account.description).to.equal('Retainer to the king of Doma')
@@ -257,12 +235,11 @@ describe('Test external auth plugins', function () {
   })
 
   it('Should not update an external auth email', async function () {
-    await updateMyUser({
-      url: server.url,
-      accessToken: cyanAccessToken,
+    await server.users.updateMe({
+      token: cyanAccessToken,
       email: 'toto@example.com',
       currentPassword: 'toto',
-      statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+      expectedStatus: HttpStatusCode.BAD_REQUEST_400
     })
   })
 
@@ -271,18 +248,16 @@ describe('Test external auth plugins', function () {
 
     await wait(5000)
 
-    await getMyUserInformation(server.url, kefkaAccessToken, HttpStatusCode.UNAUTHORIZED_401)
+    await server.users.getMyInfo({ token: kefkaAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
   })
 
   it('Should unregister external-auth-2 and do not login existing Kefka', async function () {
-    await updatePluginSettings({
-      url: server.url,
-      accessToken: server.accessToken,
+    await server.plugins.updateSettings({
       npmName: 'peertube-plugin-test-external-auth-one',
       settings: { disableKefka: true }
     })
 
-    await userLogin(server, { username: 'kefka', password: 'fake' }, HttpStatusCode.BAD_REQUEST_400)
+    await server.login.login({ user: { username: 'kefka', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
 
     await loginExternal({
       server,
@@ -292,14 +267,12 @@ describe('Test external auth plugins', function () {
         username: 'kefka'
       },
       username: 'kefka',
-      statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+      expectedStatus: HttpStatusCode.NOT_FOUND_404
     })
   })
 
   it('Should have disabled this auth', async function () {
-    const res = await getConfig(server.url)
-
-    const config: ServerConfig = res.body
+    const config = await server.config.getConfig()
 
     const auths = config.plugin.registeredExternalAuths
     expect(auths).to.have.lengthOf(7)
@@ -309,11 +282,7 @@ describe('Test external auth plugins', function () {
   })
 
   it('Should uninstall the plugin one and do not login Cyan', async function () {
-    await uninstallPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-plugin-test-external-auth-one'
-    })
+    await server.plugins.uninstall({ npmName: 'peertube-plugin-test-external-auth-one' })
 
     await loginExternal({
       server,
@@ -323,12 +292,12 @@ describe('Test external auth plugins', function () {
         username: 'cyan'
       },
       username: 'cyan',
-      statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+      expectedStatus: HttpStatusCode.NOT_FOUND_404
     })
 
-    await userLogin(server, { username: 'cyan', password: null }, HttpStatusCode.BAD_REQUEST_400)
-    await userLogin(server, { username: 'cyan', password: '' }, HttpStatusCode.BAD_REQUEST_400)
-    await userLogin(server, { username: 'cyan', password: 'fake' }, HttpStatusCode.BAD_REQUEST_400)
+    await server.login.login({ user: { username: 'cyan', password: null }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await server.login.login({ user: { username: 'cyan', password: '' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await server.login.login({ user: { username: 'cyan', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should not login kefka with another plugin', async function () {
@@ -337,7 +306,7 @@ describe('Test external auth plugins', function () {
       npmName: 'test-external-auth-two',
       authName: 'external-auth-4',
       username: 'kefka2',
-      statusCodeExpectedStep2: HttpStatusCode.BAD_REQUEST_400
+      expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400
     })
 
     await loginExternal({
@@ -345,31 +314,24 @@ describe('Test external auth plugins', function () {
       npmName: 'test-external-auth-two',
       authName: 'external-auth-4',
       username: 'kefka',
-      statusCodeExpectedStep2: HttpStatusCode.BAD_REQUEST_400
+      expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400
     })
   })
 
   it('Should not login an existing user', async function () {
-    await createUser({
-      url: server.url,
-      accessToken: server.accessToken,
-      username: 'existing_user',
-      password: 'super_password'
-    })
+    await server.users.create({ username: 'existing_user', password: 'super_password' })
 
     await loginExternal({
       server,
       npmName: 'test-external-auth-two',
       authName: 'external-auth-6',
       username: 'existing_user',
-      statusCodeExpectedStep2: HttpStatusCode.BAD_REQUEST_400
+      expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400
     })
   })
 
   it('Should display the correct configuration', async function () {
-    const res = await getConfig(server.url)
-
-    const config: ServerConfig = res.body
+    const config = await server.config.getConfig()
 
     const auths = config.plugin.registeredExternalAuths
     expect(auths).to.have.lengthOf(6)
@@ -390,9 +352,8 @@ describe('Test external auth plugins', function () {
       username: 'cid'
     })
 
-    const resLogout = await logout(server.url, resLogin.access_token)
-
-    expect(resLogout.body.redirectUrl).to.equal('https://example.com/redirectUrl')
+    const { redirectUrl } = await server.login.logout({ token: resLogin.access_token })
+    expect(redirectUrl).to.equal('https://example.com/redirectUrl')
   })
 
   it('Should call the plugin\'s onLogout method with the request', async function () {
@@ -403,8 +364,7 @@ describe('Test external auth plugins', function () {
       username: 'cid'
     })
 
-    const resLogout = await logout(server.url, resLogin.access_token)
-
-    expect(resLogout.body.redirectUrl).to.equal('https://example.com/redirectUrl?access_token=' + resLogin.access_token)
+    const { redirectUrl } = await server.login.logout({ token: resLogin.access_token })
+    expect(redirectUrl).to.equal('https://example.com/redirectUrl?access_token=' + resLogin.access_token)
   })
 })
index 644b41dea8f5e1c768d285cd199651742a657d14..02915f08cb2f8199f383cdb778b618544f014c68 100644 (file)
 
 import 'mocha'
 import * as chai from 'chai'
-import { advancedVideoChannelSearch } from '@shared/extra-utils/search/video-channels'
-import { ServerConfig } from '@shared/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 import {
-  addVideoCommentReply,
-  addVideoCommentThread,
-  advancedVideoPlaylistSearch,
-  advancedVideosSearch,
-  createLive,
-  createVideoPlaylist,
+  cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  getAccountVideos,
-  getConfig,
-  getMyVideos,
-  getPluginTestPath,
-  getVideo,
-  getVideoChannelVideos,
-  getVideoCommentThreads,
-  getVideoPlaylist,
-  getVideosList,
-  getVideosListPagination,
-  getVideoThreadComments,
-  getVideoWithToken,
-  installPlugin,
+  FIXTURE_URLS,
   makeRawRequest,
-  registerUser,
+  PeerTubeServer,
+  PluginsCommand,
   setAccessTokensToServers,
   setDefaultVideoChannel,
-  updateCustomSubConfig,
-  updateVideo,
-  uploadVideo,
-  uploadVideoAndGetId,
   waitJobs
-} from '../../../shared/extra-utils'
-import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
-import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports'
-import {
-  VideoCommentThreadTree,
-  VideoDetails,
-  VideoImport,
-  VideoImportState,
-  VideoPlaylist,
-  VideoPlaylistPrivacy,
-  VideoPrivacy
-} from '../../../shared/models/videos'
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoDetails, VideoImportState, VideoPlaylist, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test plugin filter hooks', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
   let videoUUID: string
   let threadId: number
 
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
     await setDefaultVideoChannel(servers)
     await doubleFollow(servers[0], servers[1])
 
-    await installPlugin({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      path: getPluginTestPath()
-    })
-
-    await installPlugin({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      path: getPluginTestPath('-filter-translations')
-    })
+    await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath() })
+    await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-filter-translations') })
 
     for (let i = 0; i < 10; i++) {
-      await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'default video ' + i })
+      await servers[0].videos.upload({ attributes: { name: 'default video ' + i } })
     }
 
-    const res = await getVideosList(servers[0].url)
-    videoUUID = res.body.data[0].uuid
-
-    await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-      live: { enabled: true },
-      signup: { enabled: true },
-      import: {
-        videos: {
-          http: { enabled: true },
-          torrent: { enabled: true }
+    const { data } = await servers[0].videos.list()
+    videoUUID = data[0].uuid
+
+    await servers[0].config.updateCustomSubConfig({
+      newConfig: {
+        live: { enabled: true },
+        signup: { enabled: true },
+        import: {
+          videos: {
+            http: { enabled: true },
+            torrent: { enabled: true }
+          }
         }
       }
     })
   })
 
   it('Should run filter:api.videos.list.params', async function () {
-    const res = await getVideosListPagination(servers[0].url, 0, 2)
+    const { data } = await servers[0].videos.list({ start: 0, count: 2 })
 
     // 2 plugins do +1 to the count parameter
-    expect(res.body.data).to.have.lengthOf(4)
+    expect(data).to.have.lengthOf(4)
   })
 
   it('Should run filter:api.videos.list.result', async function () {
-    const res = await getVideosListPagination(servers[0].url, 0, 0)
+    const { total } = await servers[0].videos.list({ start: 0, count: 0 })
 
     // Plugin do +1 to the total result
-    expect(res.body.total).to.equal(11)
+    expect(total).to.equal(11)
   })
 
   it('Should run filter:api.accounts.videos.list.params', async function () {
-    const res = await getAccountVideos(servers[0].url, servers[0].accessToken, 'root', 0, 2)
+    const { data } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 })
 
     // 1 plugin do +1 to the count parameter
-    expect(res.body.data).to.have.lengthOf(3)
+    expect(data).to.have.lengthOf(3)
   })
 
   it('Should run filter:api.accounts.videos.list.result', async function () {
-    const res = await getAccountVideos(servers[0].url, servers[0].accessToken, 'root', 0, 2)
+    const { total } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 })
 
     // Plugin do +2 to the total result
-    expect(res.body.total).to.equal(12)
+    expect(total).to.equal(12)
   })
 
   it('Should run filter:api.video-channels.videos.list.params', async function () {
-    const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, 'root_channel', 0, 2)
+    const { data } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 })
 
     // 1 plugin do +3 to the count parameter
-    expect(res.body.data).to.have.lengthOf(5)
+    expect(data).to.have.lengthOf(5)
   })
 
   it('Should run filter:api.video-channels.videos.list.result', async function () {
-    const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, 'root_channel', 0, 2)
+    const { total } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 })
 
     // Plugin do +3 to the total result
-    expect(res.body.total).to.equal(13)
+    expect(total).to.equal(13)
   })
 
   it('Should run filter:api.user.me.videos.list.params', async function () {
-    const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 2)
+    const { data } = await servers[0].videos.listMyVideos({ start: 0, count: 2 })
 
     // 1 plugin do +4 to the count parameter
-    expect(res.body.data).to.have.lengthOf(6)
+    expect(data).to.have.lengthOf(6)
   })
 
   it('Should run filter:api.user.me.videos.list.result', async function () {
-    const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 2)
+    const { total } = await servers[0].videos.listMyVideos({ start: 0, count: 2 })
 
     // Plugin do +4 to the total result
-    expect(res.body.total).to.equal(14)
+    expect(total).to.equal(14)
   })
 
   it('Should run filter:api.video.get.result', async function () {
-    const res = await getVideo(servers[0].url, videoUUID)
-
-    expect(res.body.name).to.contain('<3')
+    const video = await servers[0].videos.get({ id: videoUUID })
+    expect(video.name).to.contain('<3')
   })
 
   it('Should run filter:api.video.upload.accept.result', async function () {
-    await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video with bad word' }, HttpStatusCode.FORBIDDEN_403)
+    await servers[0].videos.upload({ attributes: { name: 'video with bad word' }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
   })
 
   it('Should run filter:api.live-video.create.accept.result', async function () {
     const attributes = {
       name: 'video with bad word',
       privacy: VideoPrivacy.PUBLIC,
-      channelId: servers[0].videoChannel.id
+      channelId: servers[0].store.channel.id
     }
 
-    await createLive(servers[0].url, servers[0].accessToken, attributes, HttpStatusCode.FORBIDDEN_403)
+    await servers[0].live.create({ fields: attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
   })
 
   it('Should run filter:api.video.pre-import-url.accept.result', async function () {
-    const baseAttributes = {
+    const attributes = {
       name: 'normal title',
       privacy: VideoPrivacy.PUBLIC,
-      channelId: servers[0].videoChannel.id,
-      targetUrl: getGoodVideoUrl() + 'bad'
+      channelId: servers[0].store.channel.id,
+      targetUrl: FIXTURE_URLS.goodVideo + 'bad'
     }
-    await importVideo(servers[0].url, servers[0].accessToken, baseAttributes, HttpStatusCode.FORBIDDEN_403)
+    await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
   })
 
   it('Should run filter:api.video.pre-import-torrent.accept.result', async function () {
-    const baseAttributes = {
+    const attributes = {
       name: 'bad torrent',
       privacy: VideoPrivacy.PUBLIC,
-      channelId: servers[0].videoChannel.id,
+      channelId: servers[0].store.channel.id,
       torrentfile: 'video-720p.torrent' as any
     }
-    await importVideo(servers[0].url, servers[0].accessToken, baseAttributes, HttpStatusCode.FORBIDDEN_403)
+    await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
   })
 
   it('Should run filter:api.video.post-import-url.accept.result', async function () {
@@ -196,21 +156,21 @@ describe('Test plugin filter hooks', function () {
     let videoImportId: number
 
     {
-      const baseAttributes = {
+      const attributes = {
         name: 'title with bad word',
         privacy: VideoPrivacy.PUBLIC,
-        channelId: servers[0].videoChannel.id,
-        targetUrl: getGoodVideoUrl()
+        channelId: servers[0].store.channel.id,
+        targetUrl: FIXTURE_URLS.goodVideo
       }
-      const res = await importVideo(servers[0].url, servers[0].accessToken, baseAttributes)
-      videoImportId = res.body.id
+      const body = await servers[0].imports.importVideo({ attributes })
+      videoImportId = body.id
     }
 
     await waitJobs(servers)
 
     {
-      const res = await getMyVideoImports(servers[0].url, servers[0].accessToken)
-      const videoImports = res.body.data as VideoImport[]
+      const body = await servers[0].imports.getMyVideoImports()
+      const videoImports = body.data
 
       const videoImport = videoImports.find(i => i.id === videoImportId)
 
@@ -225,21 +185,20 @@ describe('Test plugin filter hooks', function () {
     let videoImportId: number
 
     {
-      const baseAttributes = {
+      const attributes = {
         name: 'title with bad word',
         privacy: VideoPrivacy.PUBLIC,
-        channelId: servers[0].videoChannel.id,
+        channelId: servers[0].store.channel.id,
         torrentfile: 'video-720p.torrent' as any
       }
-      const res = await importVideo(servers[0].url, servers[0].accessToken, baseAttributes)
-      videoImportId = res.body.id
+      const body = await servers[0].imports.importVideo({ attributes })
+      videoImportId = body.id
     }
 
     await waitJobs(servers)
 
     {
-      const res = await getMyVideoImports(servers[0].url, servers[0].accessToken)
-      const videoImports = res.body.data as VideoImport[]
+      const { data: videoImports } = await servers[0].imports.getMyVideoImports()
 
       const videoImport = videoImports.find(i => i.id === videoImportId)
 
@@ -249,60 +208,71 @@ describe('Test plugin filter hooks', function () {
   })
 
   it('Should run filter:api.video-thread.create.accept.result', async function () {
-    await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'comment with bad word', HttpStatusCode.FORBIDDEN_403)
+    await servers[0].comments.createThread({
+      videoId: videoUUID,
+      text: 'comment with bad word',
+      expectedStatus: HttpStatusCode.FORBIDDEN_403
+    })
   })
 
   it('Should run filter:api.video-comment-reply.create.accept.result', async function () {
-    const res = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'thread')
-    threadId = res.body.comment.id
-
-    await addVideoCommentReply(
-      servers[0].url,
-      servers[0].accessToken,
-      videoUUID,
-      threadId,
-      'comment with bad word',
-      HttpStatusCode.FORBIDDEN_403
-    )
-    await addVideoCommentReply(servers[0].url, servers[0].accessToken, videoUUID, threadId, 'comment with good word', HttpStatusCode.OK_200)
+    const created = await servers[0].comments.createThread({ videoId: videoUUID, text: 'thread' })
+    threadId = created.id
+
+    await servers[0].comments.addReply({
+      videoId: videoUUID,
+      toCommentId: threadId,
+      text: 'comment with bad word',
+      expectedStatus: HttpStatusCode.FORBIDDEN_403
+    })
+    await servers[0].comments.addReply({
+      videoId: videoUUID,
+      toCommentId: threadId,
+      text: 'comment with good word',
+      expectedStatus: HttpStatusCode.OK_200
+    })
   })
 
   it('Should run filter:api.video-threads.list.params', async function () {
-    const res = await getVideoCommentThreads(servers[0].url, videoUUID, 0, 0)
+    const { data } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 })
 
     // our plugin do +1 to the count parameter
-    expect(res.body.data).to.have.lengthOf(1)
+    expect(data).to.have.lengthOf(1)
   })
 
   it('Should run filter:api.video-threads.list.result', async function () {
-    const res = await getVideoCommentThreads(servers[0].url, videoUUID, 0, 0)
+    const { total } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 })
 
     // Plugin do +1 to the total result
-    expect(res.body.total).to.equal(2)
+    expect(total).to.equal(2)
   })
 
   it('Should run filter:api.video-thread-comments.list.params')
 
   it('Should run filter:api.video-thread-comments.list.result', async function () {
-    const res = await getVideoThreadComments(servers[0].url, videoUUID, threadId)
+    const thread = await servers[0].comments.getThread({ videoId: videoUUID, threadId })
 
-    const thread = res.body as VideoCommentThreadTree
     expect(thread.comment.text.endsWith(' <3')).to.be.true
   })
 
-  describe('Should run filter:video.auto-blacklist.result', function () {
+  it('Should run filter:api.overviews.videos.list.{params,result}', async function () {
+    await servers[0].overviews.getVideos({ page: 1 })
 
-    async function checkIsBlacklisted (oldRes: any, value: boolean) {
-      const videoId = oldRes.body.video.uuid
+    // 3 because we get 3 samples per page
+    await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.params', 3)
+    await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.result', 3)
+  })
 
-      const res = await getVideoWithToken(servers[0].url, servers[0].accessToken, videoId)
-      const video: VideoDetails = res.body
+  describe('Should run filter:video.auto-blacklist.result', function () {
+
+    async function checkIsBlacklisted (id: number | string, value: boolean) {
+      const video = await servers[0].videos.getWithToken({ id })
       expect(video.blacklisted).to.equal(value)
     }
 
     it('Should blacklist on upload', async function () {
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video please blacklist me' })
-      await checkIsBlacklisted(res, true)
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video please blacklist me' } })
+      await checkIsBlacklisted(uuid, true)
     })
 
     it('Should blacklist on import', async function () {
@@ -310,60 +280,62 @@ describe('Test plugin filter hooks', function () {
 
       const attributes = {
         name: 'video please blacklist me',
-        targetUrl: getGoodVideoUrl(),
-        channelId: servers[0].videoChannel.id
+        targetUrl: FIXTURE_URLS.goodVideo,
+        channelId: servers[0].store.channel.id
       }
-      const res = await importVideo(servers[0].url, servers[0].accessToken, attributes)
-      await checkIsBlacklisted(res, true)
+      const body = await servers[0].imports.importVideo({ attributes })
+      await checkIsBlacklisted(body.video.uuid, true)
     })
 
     it('Should blacklist on update', async function () {
-      const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' })
-      const videoId = res.body.video.uuid
-      await checkIsBlacklisted(res, false)
+      const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video' } })
+      await checkIsBlacklisted(uuid, false)
 
-      await updateVideo(servers[0].url, servers[0].accessToken, videoId, { name: 'please blacklist me' })
-      await checkIsBlacklisted(res, true)
+      await servers[0].videos.update({ id: uuid, attributes: { name: 'please blacklist me' } })
+      await checkIsBlacklisted(uuid, true)
     })
 
     it('Should blacklist on remote upload', async function () {
       this.timeout(120000)
 
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'remote please blacklist me' })
+      const { uuid } = await servers[1].videos.upload({ attributes: { name: 'remote please blacklist me' } })
       await waitJobs(servers)
 
-      await checkIsBlacklisted(res, true)
+      await checkIsBlacklisted(uuid, true)
     })
 
     it('Should blacklist on remote update', async function () {
       this.timeout(120000)
 
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video' })
+      const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video' } })
       await waitJobs(servers)
 
-      const videoId = res.body.video.uuid
-      await checkIsBlacklisted(res, false)
+      await checkIsBlacklisted(uuid, false)
 
-      await updateVideo(servers[1].url, servers[1].accessToken, videoId, { name: 'please blacklist me' })
+      await servers[1].videos.update({ id: uuid, attributes: { name: 'please blacklist me' } })
       await waitJobs(servers)
 
-      await checkIsBlacklisted(res, true)
+      await checkIsBlacklisted(uuid, true)
     })
   })
 
   describe('Should run filter:api.user.signup.allowed.result', function () {
 
     it('Should run on config endpoint', async function () {
-      const res = await getConfig(servers[0].url)
-      expect((res.body as ServerConfig).signup.allowed).to.be.true
+      const body = await servers[0].config.getConfig()
+      expect(body.signup.allowed).to.be.true
     })
 
     it('Should allow a signup', async function () {
-      await registerUser(servers[0].url, 'john', 'password')
+      await servers[0].users.register({ username: 'john', password: 'password' })
     })
 
     it('Should not allow a signup', async function () {
-      const res = await registerUser(servers[0].url, 'jma', 'password', HttpStatusCode.FORBIDDEN_403)
+      const res = await servers[0].users.register({
+        username: 'jma',
+        password: 'password',
+        expectedStatus: HttpStatusCode.FORBIDDEN_403
+      })
 
       expect(res.body.error).to.equal('No jma')
     })
@@ -375,13 +347,15 @@ describe('Test plugin filter hooks', function () {
     before(async function () {
       this.timeout(120000)
 
-      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-        transcoding: {
-          webtorrent: {
-            enabled: true
-          },
-          hls: {
-            enabled: true
+      await servers[0].config.updateCustomSubConfig({
+        newConfig: {
+          transcoding: {
+            webtorrent: {
+              enabled: true
+            },
+            hls: {
+              enabled: true
+            }
           }
         }
       })
@@ -389,15 +363,14 @@ describe('Test plugin filter hooks', function () {
       const uuids: string[] = []
 
       for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) {
-        const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
+        const uuid = (await servers[0].videos.quickUpload({ name: name })).uuid
         uuids.push(uuid)
       }
 
       await waitJobs(servers)
 
       for (const uuid of uuids) {
-        const res = await getVideo(servers[0].url, uuid)
-        downloadVideos.push(res.body)
+        downloadVideos.push(await servers[0].videos.get({ id: uuid }))
       }
     })
 
@@ -437,25 +410,26 @@ describe('Test plugin filter hooks', function () {
     before(async function () {
       this.timeout(60000)
 
-      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-        transcoding: {
-          enabled: false
+      await servers[0].config.updateCustomSubConfig({
+        newConfig: {
+          transcoding: {
+            enabled: false
+          }
         }
       })
 
       for (const name of [ 'bad embed', 'good embed' ]) {
         {
-          const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
-          const res = await getVideo(servers[0].url, uuid)
-          embedVideos.push(res.body)
+          const uuid = (await servers[0].videos.quickUpload({ name: name })).uuid
+          embedVideos.push(await servers[0].videos.get({ id: uuid }))
         }
 
         {
-          const playlistAttrs = { displayName: name, videoChannelId: servers[0].videoChannel.id, privacy: VideoPlaylistPrivacy.PUBLIC }
-          const res = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs })
+          const attributes = { displayName: name, videoChannelId: servers[0].store.channel.id, privacy: VideoPlaylistPrivacy.PUBLIC }
+          const { id } = await servers[0].playlists.create({ attributes })
 
-          const resPlaylist = await getVideoPlaylist(servers[0].url, res.body.videoPlaylist.id)
-          embedPlaylists.push(resPlaylist.body)
+          const playlist = await servers[0].playlists.get({ playlistId: id })
+          embedPlaylists.push(playlist)
         }
       }
     })
@@ -474,78 +448,92 @@ describe('Test plugin filter hooks', function () {
   describe('Search filters', function () {
 
     before(async function () {
-      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
-        search: {
-          searchIndex: {
-            enabled: true,
-            isDefaultSearch: false,
-            disableLocalSearch: false
+      await servers[0].config.updateCustomSubConfig({
+        newConfig: {
+          search: {
+            searchIndex: {
+              enabled: true,
+              isDefaultSearch: false,
+              disableLocalSearch: false
+            }
           }
         }
       })
     })
 
     it('Should run filter:api.search.videos.local.list.{params,result}', async function () {
-      await advancedVideosSearch(servers[0].url, {
-        search: 'Sun Quan'
+      await servers[0].search.advancedVideoSearch({
+        search: {
+          search: 'Sun Quan'
+        }
       })
 
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.params', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.result', 1)
     })
 
     it('Should run filter:api.search.videos.index.list.{params,result}', async function () {
-      await advancedVideosSearch(servers[0].url, {
-        search: 'Sun Quan',
-        searchTarget: 'search-index'
+      await servers[0].search.advancedVideoSearch({
+        search: {
+          search: 'Sun Quan',
+          searchTarget: 'search-index'
+        }
       })
 
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.params', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.result', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.params', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.result', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.index.list.params', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.index.list.result', 1)
     })
 
     it('Should run filter:api.search.video-channels.local.list.{params,result}', async function () {
-      await advancedVideoChannelSearch(servers[0].url, {
-        search: 'Sun Ce'
+      await servers[0].search.advancedChannelSearch({
+        search: {
+          search: 'Sun Ce'
+        }
       })
 
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.params', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.result', 1)
     })
 
     it('Should run filter:api.search.video-channels.index.list.{params,result}', async function () {
-      await advancedVideoChannelSearch(servers[0].url, {
-        search: 'Sun Ce',
-        searchTarget: 'search-index'
+      await servers[0].search.advancedChannelSearch({
+        search: {
+          search: 'Sun Ce',
+          searchTarget: 'search-index'
+        }
       })
 
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.params', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.result', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.params', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.result', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.index.list.params', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.index.list.result', 1)
     })
 
     it('Should run filter:api.search.video-playlists.local.list.{params,result}', async function () {
-      await advancedVideoPlaylistSearch(servers[0].url, {
-        search: 'Sun Jian'
+      await servers[0].search.advancedPlaylistSearch({
+        search: {
+          search: 'Sun Jian'
+        }
       })
 
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.params', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.result', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.params', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.result', 1)
     })
 
     it('Should run filter:api.search.video-playlists.index.list.{params,result}', async function () {
-      await advancedVideoPlaylistSearch(servers[0].url, {
-        search: 'Sun Jian',
-        searchTarget: 'search-index'
+      await servers[0].search.advancedPlaylistSearch({
+        search: {
+          search: 'Sun Jian',
+          searchTarget: 'search-index'
+        }
       })
 
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.params', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.result', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.index.list.params', 1)
-      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.index.list.result', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.params', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.result', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.index.list.params', 1)
+      await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.index.list.result', 1)
     })
   })
 
index 4fa8caa3a33c787365eec4f43c5a9adaefa523a8..95c0cd687b69b80a3cfa400df6364e40094fc705 100644 (file)
@@ -4,31 +4,32 @@ import 'mocha'
 import * as chai from 'chai'
 import {
   cleanupTests,
-  flushAndRunServer,
-  getPluginsCSS,
-  installPlugin,
+  createSingleServer,
   makeHTMLRequest,
-  ServerInfo,
-  setAccessTokensToServers,
-  uninstallPlugin
+  PeerTubeServer,
+  PluginsCommand,
+  setAccessTokensToServers
 } from '../../../shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test plugins HTML injection', function () {
-  let server: ServerInfo = null
+  let server: PeerTubeServer = null
+  let command: PluginsCommand
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
+
+    command = server.plugins
   })
 
   it('Should not inject global css file in HTML', async function () {
     {
-      const res = await getPluginsCSS(server.url)
-      expect(res.text).to.be.empty
+      const text = await command.getCSS()
+      expect(text).to.be.empty
     }
 
     for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) {
@@ -40,17 +41,13 @@ describe('Test plugins HTML injection', function () {
   it('Should install a plugin and a theme', async function () {
     this.timeout(30000)
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-plugin-hello-world'
-    })
+    await command.install({ npmName: 'peertube-plugin-hello-world' })
   })
 
   it('Should have the correct global css', async function () {
     {
-      const res = await getPluginsCSS(server.url)
-      expect(res.text).to.contain('background-color: red')
+      const text = await command.getCSS()
+      expect(text).to.contain('background-color: red')
     }
 
     for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) {
@@ -60,15 +57,11 @@ describe('Test plugins HTML injection', function () {
   })
 
   it('Should have an empty global css on uninstall', async function () {
-    await uninstallPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-plugin-hello-world'
-    })
+    await command.uninstall({ npmName: 'peertube-plugin-hello-world' })
 
     {
-      const res = await getPluginsCSS(server.url)
-      expect(res.text).to.be.empty
+      const text = await command.getCSS()
+      expect(text).to.be.empty
     }
 
     for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) {
index cbba638c2bab3788fbe39509fe9f8f21155cd916..fde0166f9ab6fa91615a8ec2d23426dfe6a80836 100644 (file)
@@ -1,24 +1,12 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
-import {
-  getMyUserInformation,
-  getPluginTestPath,
-  installPlugin,
-  logout,
-  setAccessTokensToServers,
-  uninstallPlugin,
-  updateMyUser,
-  userLogin,
-  wait,
-  login, refreshToken, getConfig, updatePluginSettings, getUsersList
-} from '../../../shared/extra-utils'
-import { User, UserRole, ServerConfig } from '@shared/models'
 import { expect } from 'chai'
+import { cleanupTests, createSingleServer, PeerTubeServer, PluginsCommand, setAccessTokensToServers, wait } from '@shared/extra-utils'
+import { HttpStatusCode, UserRole } from '@shared/models'
 
 describe('Test id and pass auth plugins', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   let crashAccessToken: string
   let crashRefreshToken: string
@@ -29,22 +17,16 @@ describe('Test id and pass auth plugins', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
     for (const suffix of [ 'one', 'two', 'three' ]) {
-      await installPlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        path: getPluginTestPath('-id-pass-auth-' + suffix)
-      })
+      await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-id-pass-auth-' + suffix) })
     }
   })
 
   it('Should display the correct configuration', async function () {
-    const res = await getConfig(server.url)
-
-    const config: ServerConfig = res.body
+    const config = await server.config.getConfig()
 
     const auths = config.plugin.registeredIdAndPassAuths
     expect(auths).to.have.lengthOf(8)
@@ -56,15 +38,14 @@ describe('Test id and pass auth plugins', function () {
   })
 
   it('Should not login', async function () {
-    await userLogin(server, { username: 'toto', password: 'password' }, 400)
+    await server.login.login({ user: { username: 'toto', password: 'password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should login Spyro, create the user and use the token', async function () {
-    const accessToken = await userLogin(server, { username: 'spyro', password: 'spyro password' })
+    const accessToken = await server.login.getAccessToken({ username: 'spyro', password: 'spyro password' })
 
-    const res = await getMyUserInformation(server.url, accessToken)
+    const body = await server.users.getMyInfo({ token: accessToken })
 
-    const body: User = res.body
     expect(body.username).to.equal('spyro')
     expect(body.account.displayName).to.equal('Spyro the Dragon')
     expect(body.role).to.equal(UserRole.USER)
@@ -72,15 +53,14 @@ describe('Test id and pass auth plugins', function () {
 
   it('Should login Crash, create the user and use the token', async function () {
     {
-      const res = await login(server.url, server.client, { username: 'crash', password: 'crash password' })
-      crashAccessToken = res.body.access_token
-      crashRefreshToken = res.body.refresh_token
+      const body = await server.login.login({ user: { username: 'crash', password: 'crash password' } })
+      crashAccessToken = body.access_token
+      crashRefreshToken = body.refresh_token
     }
 
     {
-      const res = await getMyUserInformation(server.url, crashAccessToken)
+      const body = await server.users.getMyInfo({ token: crashAccessToken })
 
-      const body: User = res.body
       expect(body.username).to.equal('crash')
       expect(body.account.displayName).to.equal('Crash Bandicoot')
       expect(body.role).to.equal(UserRole.MODERATOR)
@@ -89,15 +69,14 @@ describe('Test id and pass auth plugins', function () {
 
   it('Should login the first Laguna, create the user and use the token', async function () {
     {
-      const res = await login(server.url, server.client, { username: 'laguna', password: 'laguna password' })
-      lagunaAccessToken = res.body.access_token
-      lagunaRefreshToken = res.body.refresh_token
+      const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } })
+      lagunaAccessToken = body.access_token
+      lagunaRefreshToken = body.refresh_token
     }
 
     {
-      const res = await getMyUserInformation(server.url, lagunaAccessToken)
+      const body = await server.users.getMyInfo({ token: lagunaAccessToken })
 
-      const body: User = res.body
       expect(body.username).to.equal('laguna')
       expect(body.account.displayName).to.equal('laguna')
       expect(body.role).to.equal(UserRole.USER)
@@ -106,51 +85,47 @@ describe('Test id and pass auth plugins', function () {
 
   it('Should refresh crash token, but not laguna token', async function () {
     {
-      const resRefresh = await refreshToken(server, crashRefreshToken)
+      const resRefresh = await server.login.refreshToken({ refreshToken: crashRefreshToken })
       crashAccessToken = resRefresh.body.access_token
       crashRefreshToken = resRefresh.body.refresh_token
 
-      const res = await getMyUserInformation(server.url, crashAccessToken)
-      const user: User = res.body
-      expect(user.username).to.equal('crash')
+      const body = await server.users.getMyInfo({ token: crashAccessToken })
+      expect(body.username).to.equal('crash')
     }
 
     {
-      await refreshToken(server, lagunaRefreshToken, 400)
+      await server.login.refreshToken({ refreshToken: lagunaRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     }
   })
 
   it('Should update Crash profile', async function () {
-    await updateMyUser({
-      url: server.url,
-      accessToken: crashAccessToken,
+    await server.users.updateMe({
+      token: crashAccessToken,
       displayName: 'Beautiful Crash',
       description: 'Mutant eastern barred bandicoot'
     })
 
-    const res = await getMyUserInformation(server.url, crashAccessToken)
+    const body = await server.users.getMyInfo({ token: crashAccessToken })
 
-    const body: User = res.body
     expect(body.account.displayName).to.equal('Beautiful Crash')
     expect(body.account.description).to.equal('Mutant eastern barred bandicoot')
   })
 
   it('Should logout Crash', async function () {
-    await logout(server.url, crashAccessToken)
+    await server.login.logout({ token: crashAccessToken })
   })
 
   it('Should have logged out Crash', async function () {
-    await waitUntilLog(server, 'On logout for auth 1 - 2')
+    await server.servers.waitUntilLog('On logout for auth 1 - 2')
 
-    await getMyUserInformation(server.url, crashAccessToken, 401)
+    await server.users.getMyInfo({ token: crashAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
   })
 
   it('Should login Crash and keep the old existing profile', async function () {
-    crashAccessToken = await userLogin(server, { username: 'crash', password: 'crash password' })
+    crashAccessToken = await server.login.getAccessToken({ username: 'crash', password: 'crash password' })
 
-    const res = await getMyUserInformation(server.url, crashAccessToken)
+    const body = await server.users.getMyInfo({ token: crashAccessToken })
 
-    const body: User = res.body
     expect(body.username).to.equal('crash')
     expect(body.account.displayName).to.equal('Beautiful Crash')
     expect(body.account.description).to.equal('Mutant eastern barred bandicoot')
@@ -162,39 +137,38 @@ describe('Test id and pass auth plugins', function () {
 
     await wait(5000)
 
-    await getMyUserInformation(server.url, lagunaAccessToken, 401)
+    await server.users.getMyInfo({ token: lagunaAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
   })
 
   it('Should reject an invalid username, email, role or display name', async function () {
-    await userLogin(server, { username: 'ward', password: 'ward password' }, 400)
-    await waitUntilLog(server, 'valid username')
+    const command = server.login
 
-    await userLogin(server, { username: 'kiros', password: 'kiros password' }, 400)
-    await waitUntilLog(server, 'valid display name')
+    await command.login({ user: { username: 'ward', password: 'ward password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await server.servers.waitUntilLog('valid username')
 
-    await userLogin(server, { username: 'raine', password: 'raine password' }, 400)
-    await waitUntilLog(server, 'valid role')
+    await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await server.servers.waitUntilLog('valid display name')
 
-    await userLogin(server, { username: 'ellone', password: 'elonne password' }, 400)
-    await waitUntilLog(server, 'valid email')
+    await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await server.servers.waitUntilLog('valid role')
+
+    await command.login({ user: { username: 'ellone', password: 'elonne password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await server.servers.waitUntilLog('valid email')
   })
 
   it('Should unregister spyro-auth and do not login existing Spyro', async function () {
-    await updatePluginSettings({
-      url: server.url,
-      accessToken: server.accessToken,
+    await server.plugins.updateSettings({
       npmName: 'peertube-plugin-test-id-pass-auth-one',
       settings: { disableSpyro: true }
     })
 
-    await userLogin(server, { username: 'spyro', password: 'spyro password' }, 400)
-    await userLogin(server, { username: 'spyro', password: 'fake' }, 400)
+    const command = server.login
+    await command.login({ user: { username: 'spyro', password: 'spyro password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    await command.login({ user: { username: 'spyro', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should have disabled this auth', async function () {
-    const res = await getConfig(server.url)
-
-    const config: ServerConfig = res.body
+    const config = await server.config.getConfig()
 
     const auths = config.plugin.registeredIdAndPassAuths
     expect(auths).to.have.lengthOf(7)
@@ -204,19 +178,16 @@ describe('Test id and pass auth plugins', function () {
   })
 
   it('Should uninstall the plugin one and do not login existing Crash', async function () {
-    await uninstallPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-plugin-test-id-pass-auth-one'
-    })
+    await server.plugins.uninstall({ npmName: 'peertube-plugin-test-id-pass-auth-one' })
 
-    await userLogin(server, { username: 'crash', password: 'crash password' }, 400)
+    await server.login.login({
+      user: { username: 'crash', password: 'crash password' },
+      expectedStatus: HttpStatusCode.BAD_REQUEST_400
+    })
   })
 
   it('Should display the correct configuration', async function () {
-    const res = await getConfig(server.url)
-
-    const config: ServerConfig = res.body
+    const config = await server.config.getConfig()
 
     const auths = config.plugin.registeredIdAndPassAuths
     expect(auths).to.have.lengthOf(6)
@@ -226,13 +197,11 @@ describe('Test id and pass auth plugins', function () {
   })
 
   it('Should display plugin auth information in users list', async function () {
-    const res = await getUsersList(server.url, server.accessToken)
-
-    const users: User[] = res.body.data
+    const { data } = await server.users.list()
 
-    const root = users.find(u => u.username === 'root')
-    const crash = users.find(u => u.username === 'crash')
-    const laguna = users.find(u => u.username === 'laguna')
+    const root = data.find(u => u.username === 'root')
+    const crash = data.find(u => u.username === 'crash')
+    const laguna = data.find(u => u.username === 'laguna')
 
     expect(root.pluginAuth).to.be.null
     expect(crash.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-one')
index 0296d6eb73a244e2e367ce2db803f9c014de022a..5d16b28a4eecab3f8e097fc78a82db88c3a071f8 100644 (file)
@@ -1,25 +1,22 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
+import { expect } from 'chai'
 import {
   checkVideoFilesWereRemoved,
+  cleanupTests,
+  createMultipleServers,
   doubleFollow,
-  getPluginTestPath,
-  getVideo,
-  installPlugin,
+  makeGetRequest,
   makePostBodyRequest,
+  PeerTubeServer,
+  PluginsCommand,
   setAccessTokensToServers,
-  uploadVideoAndGetId,
-  viewVideo,
-  getVideosList,
-  waitJobs,
-  makeGetRequest
-} from '../../../shared/extra-utils'
-import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
-import { expect } from 'chai'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+  waitJobs
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
-function postCommand (server: ServerInfo, command: string, bodyArg?: object) {
+function postCommand (server: PeerTubeServer, command: string, bodyArg?: object) {
   const body = { command }
   if (bodyArg) Object.assign(body, bodyArg)
 
@@ -27,54 +24,50 @@ function postCommand (server: ServerInfo, command: string, bodyArg?: object) {
     url: server.url,
     path: '/plugins/test-four/router/commander',
     fields: body,
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
+    expectedStatus: HttpStatusCode.NO_CONTENT_204
   })
 }
 
 describe('Test plugin helpers', function () {
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
 
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await createMultipleServers(2)
     await setAccessTokensToServers(servers)
 
     await doubleFollow(servers[0], servers[1])
 
-    await installPlugin({
-      url: servers[0].url,
-      accessToken: servers[0].accessToken,
-      path: getPluginTestPath('-four')
-    })
+    await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-four') })
   })
 
   describe('Logger', function () {
 
     it('Should have logged things', async function () {
-      await waitUntilLog(servers[0], 'localhost:' + servers[0].port + ' peertube-plugin-test-four', 1, false)
-      await waitUntilLog(servers[0], 'Hello world from plugin four', 1)
+      await servers[0].servers.waitUntilLog('localhost:' + servers[0].port + ' peertube-plugin-test-four', 1, false)
+      await servers[0].servers.waitUntilLog('Hello world from plugin four', 1)
     })
   })
 
   describe('Database', function () {
 
     it('Should have made a query', async function () {
-      await waitUntilLog(servers[0], `root email is admin${servers[0].internalServerNumber}@example.com`)
+      await servers[0].servers.waitUntilLog(`root email is admin${servers[0].internalServerNumber}@example.com`)
     })
   })
 
   describe('Config', function () {
 
     it('Should have the correct webserver url', async function () {
-      await waitUntilLog(servers[0], `server url is http://localhost:${servers[0].port}`)
+      await servers[0].servers.waitUntilLog(`server url is http://localhost:${servers[0].port}`)
     })
 
     it('Should have the correct config', async function () {
       const res = await makeGetRequest({
         url: servers[0].url,
         path: '/plugins/test-four/router/server-config',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body.serverConfig).to.exist
@@ -85,7 +78,7 @@ describe('Test plugin helpers', function () {
   describe('Server', function () {
 
     it('Should get the server actor', async function () {
-      await waitUntilLog(servers[0], 'server actor name is peertube')
+      await servers[0].servers.waitUntilLog('server actor name is peertube')
     })
   })
 
@@ -95,7 +88,7 @@ describe('Test plugin helpers', function () {
       const res = await makeGetRequest({
         url: servers[0].url,
         path: '/plugins/test-four/router/static-route',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body.staticRoute).to.equal('/plugins/test-four/0.0.1/static/')
@@ -107,7 +100,7 @@ describe('Test plugin helpers', function () {
       const res = await makeGetRequest({
         url: servers[0].url,
         path: baseRouter + 'router-route',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body.routerRoute).to.equal(baseRouter)
@@ -120,7 +113,7 @@ describe('Test plugin helpers', function () {
       await makeGetRequest({
         url: servers[0].url,
         path: '/plugins/test-four/router/user',
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     })
 
@@ -129,7 +122,7 @@ describe('Test plugin helpers', function () {
         url: servers[0].url,
         token: servers[0].accessToken,
         path: '/plugins/test-four/router/user',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body.username).to.equal('root')
@@ -147,59 +140,54 @@ describe('Test plugin helpers', function () {
       this.timeout(60000)
 
       {
-        const res = await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' })
+        const res = await servers[0].videos.quickUpload({ name: 'video server 1' })
         videoUUIDServer1 = res.uuid
       }
 
       {
-        await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' })
+        await servers[1].videos.quickUpload({ name: 'video server 2' })
       }
 
       await waitJobs(servers)
 
-      const res = await getVideosList(servers[0].url)
-      const videos = res.body.data
+      const { data } = await servers[0].videos.list()
 
-      expect(videos).to.have.lengthOf(2)
+      expect(data).to.have.lengthOf(2)
     })
 
     it('Should mute server 2', async function () {
       this.timeout(10000)
       await postCommand(servers[0], 'blockServer', { hostToBlock: `localhost:${servers[1].port}` })
 
-      const res = await getVideosList(servers[0].url)
-      const videos = res.body.data
+      const { data } = await servers[0].videos.list()
 
-      expect(videos).to.have.lengthOf(1)
-      expect(videos[0].name).to.equal('video server 1')
+      expect(data).to.have.lengthOf(1)
+      expect(data[0].name).to.equal('video server 1')
     })
 
     it('Should unmute server 2', async function () {
       await postCommand(servers[0], 'unblockServer', { hostToUnblock: `localhost:${servers[1].port}` })
 
-      const res = await getVideosList(servers[0].url)
-      const videos = res.body.data
+      const { data } = await servers[0].videos.list()
 
-      expect(videos).to.have.lengthOf(2)
+      expect(data).to.have.lengthOf(2)
     })
 
     it('Should mute account of server 2', async function () {
       await postCommand(servers[0], 'blockAccount', { handleToBlock: `root@localhost:${servers[1].port}` })
 
-      const res = await getVideosList(servers[0].url)
-      const videos = res.body.data
+      const { data } = await servers[0].videos.list()
 
-      expect(videos).to.have.lengthOf(1)
-      expect(videos[0].name).to.equal('video server 1')
+      expect(data).to.have.lengthOf(1)
+      expect(data[0].name).to.equal('video server 1')
     })
 
     it('Should unmute account of server 2', async function () {
       await postCommand(servers[0], 'unblockAccount', { handleToUnblock: `root@localhost:${servers[1].port}` })
 
-      const res = await getVideosList(servers[0].url)
-      const videos = res.body.data
+      const { data } = await servers[0].videos.list()
 
-      expect(videos).to.have.lengthOf(2)
+      expect(data).to.have.lengthOf(2)
     })
 
     it('Should blacklist video', async function () {
@@ -210,11 +198,10 @@ describe('Test plugin helpers', function () {
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-        const videos = res.body.data
+        const { data } = await server.videos.list()
 
-        expect(videos).to.have.lengthOf(1)
-        expect(videos[0].name).to.equal('video server 2')
+        expect(data).to.have.lengthOf(1)
+        expect(data[0].name).to.equal('video server 2')
       }
     })
 
@@ -226,10 +213,9 @@ describe('Test plugin helpers', function () {
       await waitJobs(servers)
 
       for (const server of servers) {
-        const res = await getVideosList(server.url)
-        const videos = res.body.data
+        const { data } = await server.videos.list()
 
-        expect(videos).to.have.lengthOf(2)
+        expect(data).to.have.lengthOf(2)
       }
     })
   })
@@ -238,7 +224,7 @@ describe('Test plugin helpers', function () {
     let videoUUID: string
 
     before(async () => {
-      const res = await uploadVideoAndGetId({ server: servers[0], videoName: 'video1' })
+      const res = await servers[0].videos.quickUpload({ name: 'video1' })
       videoUUID = res.uuid
     })
 
@@ -246,25 +232,25 @@ describe('Test plugin helpers', function () {
       this.timeout(40000)
 
       // Should not throw -> video exists
-      await getVideo(servers[0].url, videoUUID)
+      const video = await servers[0].videos.get({ id: videoUUID })
       // Should delete the video
-      await viewVideo(servers[0].url, videoUUID)
+      await servers[0].videos.view({ id: videoUUID })
 
-      await waitUntilLog(servers[0], 'Video deleted by plugin four.')
+      await servers[0].servers.waitUntilLog('Video deleted by plugin four.')
 
       try {
         // Should throw because the video should have been deleted
-        await getVideo(servers[0].url, videoUUID)
+        await servers[0].videos.get({ id: videoUUID })
         throw new Error('Video exists')
       } catch (err) {
         if (err.message.includes('exists')) throw err
       }
 
-      await checkVideoFilesWereRemoved(videoUUID, servers[0].internalServerNumber)
+      await checkVideoFilesWereRemoved({ server: servers[0], video })
     })
 
     it('Should have fetched the video by URL', async function () {
-      await waitUntilLog(servers[0], `video from DB uuid is ${videoUUID}`)
+      await servers[0].servers.waitUntilLog(`video from DB uuid is ${videoUUID}`)
     })
   })
 
index 24e6a1e834701c97c4d6b088a66135a8753871d1..b1ac9e2fe72a1550c16c8215e513b7999e8a69d6 100644 (file)
@@ -1,19 +1,20 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
+import { expect } from 'chai'
 import {
-  getPluginTestPath,
-  installPlugin,
+  cleanupTests,
+  createSingleServer,
   makeGetRequest,
   makePostBodyRequest,
-  setAccessTokensToServers, uninstallPlugin
-} from '../../../shared/extra-utils'
-import { expect } from 'chai'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+  PeerTubeServer,
+  PluginsCommand,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test plugin helpers', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
   const basePaths = [
     '/plugins/test-five/router/',
     '/plugins/test-five/0.0.1/router/'
@@ -22,14 +23,10 @@ describe('Test plugin helpers', function () {
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      path: getPluginTestPath('-five')
-    })
+    await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-five') })
   })
 
   it('Should answer "pong"', async function () {
@@ -37,7 +34,7 @@ describe('Test plugin helpers', function () {
       const res = await makeGetRequest({
         url: server.url,
         path: path + 'ping',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body.message).to.equal('pong')
@@ -50,7 +47,7 @@ describe('Test plugin helpers', function () {
         url: server.url,
         path: path + 'is-authenticated',
         token: server.accessToken,
-        statusCodeExpected: 200
+        expectedStatus: 200
       })
 
       expect(res.body.isAuthenticated).to.equal(true)
@@ -58,7 +55,7 @@ describe('Test plugin helpers', function () {
       const secRes = await makeGetRequest({
         url: server.url,
         path: path + 'is-authenticated',
-        statusCodeExpected: 200
+        expectedStatus: 200
       })
 
       expect(secRes.body.isAuthenticated).to.equal(false)
@@ -77,7 +74,7 @@ describe('Test plugin helpers', function () {
         url: server.url,
         path: path + 'form/post/mirror',
         fields: body,
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       expect(res.body).to.deep.equal(body)
@@ -85,24 +82,20 @@ describe('Test plugin helpers', function () {
   })
 
   it('Should remove the plugin and remove the routes', async function () {
-    await uninstallPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-plugin-test-five'
-    })
+    await server.plugins.uninstall({ npmName: 'peertube-plugin-test-five' })
 
     for (const path of basePaths) {
       await makeGetRequest({
         url: server.url,
         path: path + 'ping',
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
 
       await makePostBodyRequest({
         url: server.url,
         path: path + 'ping',
         fields: {},
-        statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
       })
     }
   })
index 3c46b2585cc9050d53cf396f9100b59f7824d552..e20c36dbacb38f8daca9973f03d56b1f72052e15 100644 (file)
@@ -4,37 +4,32 @@ import 'mocha'
 import { expect } from 'chai'
 import { pathExists, readdir, readFile } from 'fs-extra'
 import { join } from 'path'
-import { HttpStatusCode } from '@shared/core-utils'
 import {
-  buildServerDirectory,
-  getPluginTestPath,
-  installPlugin,
+  cleanupTests,
+  createSingleServer,
   makeGetRequest,
-  setAccessTokensToServers,
-  uninstallPlugin
-} from '../../../shared/extra-utils'
-import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
+  PeerTubeServer,
+  PluginsCommand,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test plugin storage', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      path: getPluginTestPath('-six')
-    })
+    await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-six') })
   })
 
   describe('DB storage', function () {
 
     it('Should correctly store a subkey', async function () {
-      await waitUntilLog(server, 'superkey stored value is toto')
+      await server.servers.waitUntilLog('superkey stored value is toto')
     })
   })
 
@@ -50,12 +45,12 @@ describe('Test plugin storage', function () {
     }
 
     before(function () {
-      dataPath = buildServerDirectory(server, 'plugins/data')
+      dataPath = server.servers.buildDirectory('plugins/data')
       pluginDataPath = join(dataPath, 'peertube-plugin-test-six')
     })
 
     it('Should have created the directory on install', async function () {
-      const dataPath = buildServerDirectory(server, 'plugins/data')
+      const dataPath = server.servers.buildDirectory('plugins/data')
       const pluginDataPath = join(dataPath, 'peertube-plugin-test-six')
 
       expect(await pathExists(dataPath)).to.be.true
@@ -68,7 +63,7 @@ describe('Test plugin storage', function () {
         url: server.url,
         token: server.accessToken,
         path: '/plugins/test-six/router/create-file',
-        statusCodeExpected: HttpStatusCode.OK_200
+        expectedStatus: HttpStatusCode.OK_200
       })
 
       const content = await getFileContent()
@@ -76,22 +71,14 @@ describe('Test plugin storage', function () {
     })
 
     it('Should still have the file after an uninstallation', async function () {
-      await uninstallPlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        npmName: 'peertube-plugin-test-six'
-      })
+      await server.plugins.uninstall({ npmName: 'peertube-plugin-test-six' })
 
       const content = await getFileContent()
       expect(content).to.equal('Prince Ali')
     })
 
     it('Should still have the file after the reinstallation', async function () {
-      await installPlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        path: getPluginTestPath('-six')
-      })
+      await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-six') })
 
       const content = await getFileContent()
       expect(content).to.equal('Prince Ali')
index eefb2294df018dd45661147ce47e0c7d0b7fb8c2..93637e3cea58d3307fb417823b7bbbb44a82787c 100644 (file)
@@ -2,79 +2,73 @@
 
 import 'mocha'
 import { expect } from 'chai'
-import { join } from 'path'
 import { getAudioStream, getVideoFileFPS, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils'
-import { ServerConfig, VideoDetails, VideoPrivacy } from '@shared/models'
 import {
-  buildServerDirectory,
-  createLive,
-  getConfig,
-  getPluginTestPath,
-  getVideo,
-  installPlugin,
-  sendRTMPStreamInVideo,
+  cleanupTests,
+  createSingleServer,
+  PeerTubeServer,
+  PluginsCommand,
   setAccessTokensToServers,
   setDefaultVideoChannel,
   testFfmpegStreamError,
-  uninstallPlugin,
-  updateCustomSubConfig,
-  uploadVideoAndGetId,
-  waitJobs,
-  waitUntilLivePublished
-} from '../../../shared/extra-utils'
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
-
-async function createLiveWrapper (server: ServerInfo) {
+  waitJobs
+} from '@shared/extra-utils'
+import { VideoPrivacy } from '@shared/models'
+
+async function createLiveWrapper (server: PeerTubeServer) {
   const liveAttributes = {
     name: 'live video',
-    channelId: server.videoChannel.id,
+    channelId: server.store.channel.id,
     privacy: VideoPrivacy.PUBLIC
   }
 
-  const res = await createLive(server.url, server.accessToken, liveAttributes)
-  return res.body.video.uuid
+  const { uuid } = await server.live.create({ fields: liveAttributes })
+
+  return uuid
 }
 
-function updateConf (server: ServerInfo, vodProfile: string, liveProfile: string) {
-  return updateCustomSubConfig(server.url, server.accessToken, {
-    transcoding: {
-      enabled: true,
-      profile: vodProfile,
-      hls: {
-        enabled: true
-      },
-      webtorrent: {
-        enabled: true
-      },
-      resolutions: {
-        '240p': true,
-        '360p': false,
-        '480p': false,
-        '720p': true
-      }
-    },
-    live: {
+function updateConf (server: PeerTubeServer, vodProfile: string, liveProfile: string) {
+  return server.config.updateCustomSubConfig({
+    newConfig: {
       transcoding: {
-        profile: liveProfile,
         enabled: true,
+        profile: vodProfile,
+        hls: {
+          enabled: true
+        },
+        webtorrent: {
+          enabled: true
+        },
         resolutions: {
           '240p': true,
           '360p': false,
           '480p': false,
           '720p': true
         }
+      },
+      live: {
+        transcoding: {
+          profile: liveProfile,
+          enabled: true,
+          resolutions: {
+            '240p': true,
+            '360p': false,
+            '480p': false,
+            '720p': true
+          }
+        }
       }
     }
   })
 }
 
 describe('Test transcoding plugins', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   before(async function () {
     this.timeout(60000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
     await setDefaultVideoChannel([ server ])
 
@@ -84,8 +78,7 @@ describe('Test transcoding plugins', function () {
   describe('When using a plugin adding profiles to existing encoders', function () {
 
     async function checkVideoFPS (uuid: string, type: 'above' | 'below', fps: number) {
-      const res = await getVideo(server.url, uuid)
-      const video = res.body as VideoDetails
+      const video = await server.videos.get({ id: uuid })
       const files = video.files.concat(...video.streamingPlaylists.map(p => p.files))
 
       for (const file of files) {
@@ -109,134 +102,132 @@ describe('Test transcoding plugins', function () {
     }
 
     before(async function () {
-      await installPlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        path: getPluginTestPath('-transcoding-one')
-      })
+      await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-transcoding-one') })
     })
 
     it('Should have the appropriate available profiles', async function () {
-      const res = await getConfig(server.url)
-      const config = res.body as ServerConfig
+      const config = await server.config.getConfig()
 
       expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'input-options-vod', 'bad-scale-vod' ])
-      expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live', 'input-options-live', 'bad-scale-live' ])
+      expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'high-live', 'input-options-live', 'bad-scale-live' ])
     })
 
-    it('Should not use the plugin profile if not chosen by the admin', async function () {
-      this.timeout(240000)
+    describe('VOD', function () {
 
-      const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
-      await waitJobs([ server ])
+      it('Should not use the plugin profile if not chosen by the admin', async function () {
+        this.timeout(240000)
 
-      await checkVideoFPS(videoUUID, 'above', 20)
-    })
+        const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid
+        await waitJobs([ server ])
 
-    it('Should use the vod profile', async function () {
-      this.timeout(240000)
+        await checkVideoFPS(videoUUID, 'above', 20)
+      })
 
-      await updateConf(server, 'low-vod', 'default')
+      it('Should use the vod profile', async function () {
+        this.timeout(240000)
 
-      const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
-      await waitJobs([ server ])
+        await updateConf(server, 'low-vod', 'default')
 
-      await checkVideoFPS(videoUUID, 'below', 12)
-    })
+        const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid
+        await waitJobs([ server ])
 
-    it('Should apply input options in vod profile', async function () {
-      this.timeout(240000)
+        await checkVideoFPS(videoUUID, 'below', 12)
+      })
 
-      await updateConf(server, 'input-options-vod', 'default')
+      it('Should apply input options in vod profile', async function () {
+        this.timeout(240000)
 
-      const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
-      await waitJobs([ server ])
+        await updateConf(server, 'input-options-vod', 'default')
 
-      await checkVideoFPS(videoUUID, 'below', 6)
-    })
+        const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid
+        await waitJobs([ server ])
 
-    it('Should apply the scale filter in vod profile', async function () {
-      this.timeout(240000)
+        await checkVideoFPS(videoUUID, 'below', 6)
+      })
 
-      await updateConf(server, 'bad-scale-vod', 'default')
+      it('Should apply the scale filter in vod profile', async function () {
+        this.timeout(240000)
 
-      const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
-      await waitJobs([ server ])
+        await updateConf(server, 'bad-scale-vod', 'default')
 
-      // Transcoding failed
-      const res = await getVideo(server.url, videoUUID)
-      const video: VideoDetails = res.body
+        const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid
+        await waitJobs([ server ])
 
-      expect(video.files).to.have.lengthOf(1)
-      expect(video.streamingPlaylists).to.have.lengthOf(0)
+        // Transcoding failed
+        const video = await server.videos.get({ id: videoUUID })
+        expect(video.files).to.have.lengthOf(1)
+        expect(video.streamingPlaylists).to.have.lengthOf(0)
+      })
     })
 
-    it('Should not use the plugin profile if not chosen by the admin', async function () {
-      this.timeout(240000)
+    describe('Live', function () {
 
-      const liveVideoId = await createLiveWrapper(server)
+      it('Should not use the plugin profile if not chosen by the admin', async function () {
+        this.timeout(240000)
 
-      await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
-      await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
-      await waitJobs([ server ])
+        const liveVideoId = await createLiveWrapper(server)
 
-      await checkLiveFPS(liveVideoId, 'above', 20)
-    })
+        await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' })
+        await server.live.waitUntilPublished({ videoId: liveVideoId })
+        await waitJobs([ server ])
 
-    it('Should use the live profile', async function () {
-      this.timeout(240000)
+        await checkLiveFPS(liveVideoId, 'above', 20)
+      })
 
-      await updateConf(server, 'low-vod', 'low-live')
+      it('Should use the live profile', async function () {
+        this.timeout(240000)
 
-      const liveVideoId = await createLiveWrapper(server)
+        await updateConf(server, 'low-vod', 'high-live')
 
-      await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
-      await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
-      await waitJobs([ server ])
+        const liveVideoId = await createLiveWrapper(server)
 
-      await checkLiveFPS(liveVideoId, 'below', 12)
-    })
+        await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' })
+        await server.live.waitUntilPublished({ videoId: liveVideoId })
+        await waitJobs([ server ])
 
-    it('Should apply the input options on live profile', async function () {
-      this.timeout(240000)
+        await checkLiveFPS(liveVideoId, 'above', 45)
+      })
 
-      await updateConf(server, 'low-vod', 'input-options-live')
+      it('Should apply the input options on live profile', async function () {
+        this.timeout(240000)
 
-      const liveVideoId = await createLiveWrapper(server)
+        await updateConf(server, 'low-vod', 'input-options-live')
 
-      await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
-      await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
-      await waitJobs([ server ])
+        const liveVideoId = await createLiveWrapper(server)
 
-      await checkLiveFPS(liveVideoId, 'below', 6)
-    })
+        await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' })
+        await server.live.waitUntilPublished({ videoId: liveVideoId })
+        await waitJobs([ server ])
 
-    it('Should apply the scale filter name on live profile', async function () {
-      this.timeout(240000)
+        await checkLiveFPS(liveVideoId, 'above', 45)
+      })
 
-      await updateConf(server, 'low-vod', 'bad-scale-live')
+      it('Should apply the scale filter name on live profile', async function () {
+        this.timeout(240000)
 
-      const liveVideoId = await createLiveWrapper(server)
+        await updateConf(server, 'low-vod', 'bad-scale-live')
 
-      const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
-      await testFfmpegStreamError(command, true)
-    })
+        const liveVideoId = await createLiveWrapper(server)
 
-    it('Should default to the default profile if the specified profile does not exist', async function () {
-      this.timeout(240000)
+        const command = await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' })
+        await testFfmpegStreamError(command, true)
+      })
 
-      await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-transcoding-one' })
+      it('Should default to the default profile if the specified profile does not exist', async function () {
+        this.timeout(240000)
 
-      const res = await getConfig(server.url)
-      const config = res.body as ServerConfig
+        await server.plugins.uninstall({ npmName: 'peertube-plugin-test-transcoding-one' })
 
-      expect(config.transcoding.availableProfiles).to.deep.equal([ 'default' ])
-      expect(config.live.transcoding.availableProfiles).to.deep.equal([ 'default' ])
+        const config = await server.config.getConfig()
 
-      const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
-      await waitJobs([ server ])
+        expect(config.transcoding.availableProfiles).to.deep.equal([ 'default' ])
+        expect(config.live.transcoding.availableProfiles).to.deep.equal([ 'default' ])
+
+        const videoUUID = (await server.videos.quickUpload({ name: 'video', fixture: 'video_very_short_240p.mp4' })).uuid
+        await waitJobs([ server ])
 
-      await checkVideoFPS(videoUUID, 'above', 20)
+        await checkVideoFPS(videoUUID, 'above', 20)
+      })
     })
 
   })
@@ -244,11 +235,7 @@ describe('Test transcoding plugins', function () {
   describe('When using a plugin adding new encoders', function () {
 
     before(async function () {
-      await installPlugin({
-        url: server.url,
-        accessToken: server.accessToken,
-        path: getPluginTestPath('-transcoding-two')
-      })
+      await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-transcoding-two') })
 
       await updateConf(server, 'test-vod-profile', 'test-live-profile')
     })
@@ -256,10 +243,12 @@ describe('Test transcoding plugins', function () {
     it('Should use the new vod encoders', async function () {
       this.timeout(240000)
 
-      const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video', fixture: 'video_short_240p.mp4' })).uuid
+      const videoUUID = (await server.videos.quickUpload({ name: 'video', fixture: 'video_very_short_240p.mp4' })).uuid
       await waitJobs([ server ])
 
-      const path = buildServerDirectory(server, join('videos', videoUUID + '-240.mp4'))
+      const video = await server.videos.get({ id: videoUUID })
+
+      const path = server.servers.buildWebTorrentFilePath(video.files[0].fileUrl)
       const audioProbe = await getAudioStream(path)
       expect(audioProbe.audioStream.codec_name).to.equal('opus')
 
@@ -272,8 +261,8 @@ describe('Test transcoding plugins', function () {
 
       const liveVideoId = await createLiveWrapper(server)
 
-      await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
-      await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
+      await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
+      await server.live.waitUntilPublished({ videoId: liveVideoId })
       await waitJobs([ server ])
 
       const playlistUrl = `${server.url}/static/streaming-playlists/hls/${liveVideoId}/0.m3u8`
index 74ca82e2f23f0ad878f55337a1c23c12cbbbfb0b..6bf2fda9b3610034cf587fef9e768cbf127d14d9 100644 (file)
@@ -1,42 +1,36 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
+import { expect } from 'chai'
 import {
   cleanupTests,
-  flushAndRunServer,
-  getPluginTestPath,
+  createSingleServer,
   makeGetRequest,
-  installPlugin,
-  uninstallPlugin,
-  ServerInfo,
+  PeerTubeServer,
+  PluginsCommand,
   setAccessTokensToServers
-} from '../../../shared/extra-utils'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-import { expect } from 'chai'
+} from '@shared/extra-utils'
+import { HttpStatusCode } from '@shared/models'
 
 describe('Test plugins module unloading', function () {
-  let server: ServerInfo = null
+  let server: PeerTubeServer = null
   const requestPath = '/plugins/test-unloading/router/get'
   let value: string = null
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      path: getPluginTestPath('-unloading')
-    })
+    await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-unloading') })
   })
 
   it('Should return a numeric value', async function () {
     const res = await makeGetRequest({
       url: server.url,
       path: requestPath,
-      statusCodeExpected: HttpStatusCode.OK_200
+      expectedStatus: HttpStatusCode.OK_200
     })
 
     expect(res.body.message).to.match(/^\d+$/)
@@ -47,36 +41,29 @@ describe('Test plugins module unloading', function () {
     const res = await makeGetRequest({
       url: server.url,
       path: requestPath,
-      statusCodeExpected: HttpStatusCode.OK_200
+      expectedStatus: HttpStatusCode.OK_200
     })
 
     expect(res.body.message).to.be.equal(value)
   })
 
   it('Should uninstall the plugin and free the route', async function () {
-    await uninstallPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      npmName: 'peertube-plugin-test-unloading'
-    })
+    await server.plugins.uninstall({ npmName: 'peertube-plugin-test-unloading' })
 
     await makeGetRequest({
       url: server.url,
       path: requestPath,
-      statusCodeExpected: HttpStatusCode.NOT_FOUND_404
+      expectedStatus: HttpStatusCode.NOT_FOUND_404
     })
   })
 
   it('Should return a different numeric value', async function () {
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      path: getPluginTestPath('-unloading')
-    })
+    await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-unloading') })
+
     const res = await makeGetRequest({
       url: server.url,
       path: requestPath,
-      statusCodeExpected: HttpStatusCode.OK_200
+      expectedStatus: HttpStatusCode.OK_200
     })
 
     expect(res.body.message).to.match(/^\d+$/)
index 9fd2ba1c5e0b8181ba4da78b6cba602c319aca29..8b25c6b7589455fee1022c918868d513e3f0b6c4 100644 (file)
@@ -1,50 +1,37 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
-import {
-  getPluginTestPath,
-  getPluginTranslations,
-  installPlugin,
-  setAccessTokensToServers,
-  uninstallPlugin
-} from '../../../shared/extra-utils'
+import * as chai from 'chai'
+import { cleanupTests, createSingleServer, PeerTubeServer, PluginsCommand, setAccessTokensToServers } from '@shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test plugin translations', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
+  let command: PluginsCommand
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      path: getPluginTestPath()
-    })
+    command = server.plugins
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      path: getPluginTestPath('-filter-translations')
-    })
+    await command.install({ path: PluginsCommand.getPluginTestPath() })
+    await command.install({ path: PluginsCommand.getPluginTestPath('-filter-translations') })
   })
 
   it('Should not have translations for locale pt', async function () {
-    const res = await getPluginTranslations({ url: server.url, locale: 'pt' })
+    const body = await command.getTranslations({ locale: 'pt' })
 
-    expect(res.body).to.deep.equal({})
+    expect(body).to.deep.equal({})
   })
 
   it('Should have translations for locale fr', async function () {
-    const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' })
+    const body = await command.getTranslations({ locale: 'fr-FR' })
 
-    expect(res.body).to.deep.equal({
+    expect(body).to.deep.equal({
       'peertube-plugin-test': {
         Hi: 'Coucou'
       },
@@ -55,9 +42,9 @@ describe('Test plugin translations', function () {
   })
 
   it('Should have translations of locale it', async function () {
-    const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' })
+    const body = await command.getTranslations({ locale: 'it-IT' })
 
-    expect(res.body).to.deep.equal({
+    expect(body).to.deep.equal({
       'peertube-plugin-test-filter-translations': {
         'Hello world': 'Ciao, mondo!'
       }
@@ -65,12 +52,12 @@ describe('Test plugin translations', function () {
   })
 
   it('Should remove the plugin and remove the locales', async function () {
-    await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-filter-translations' })
+    await command.uninstall({ npmName: 'peertube-plugin-test-filter-translations' })
 
     {
-      const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' })
+      const body = await command.getTranslations({ locale: 'fr-FR' })
 
-      expect(res.body).to.deep.equal({
+      expect(body).to.deep.equal({
         'peertube-plugin-test': {
           Hi: 'Coucou'
         }
@@ -78,9 +65,9 @@ describe('Test plugin translations', function () {
     }
 
     {
-      const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' })
+      const body = await command.getTranslations({ locale: 'it-IT' })
 
-      expect(res.body).to.deep.equal({})
+      expect(body).to.deep.equal({})
     }
   })
 
index eb014c596deb492c975541281d28cee114e33558..19cba6c2c85d34651c7823cc8a5cb42b64bd617a 100644 (file)
@@ -1,44 +1,33 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
+import * as chai from 'chai'
 import {
-  createVideoPlaylist,
-  getPluginTestPath,
-  getVideo,
-  getVideoCategories,
-  getVideoLanguages,
-  getVideoLicences, getVideoPlaylistPrivacies, getVideoPrivacies,
-  installPlugin,
-  setAccessTokensToServers,
-  uninstallPlugin,
-  uploadVideo
-} from '../../../shared/extra-utils'
-import { VideoDetails, VideoPlaylistPrivacy } from '../../../shared/models/videos'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+  cleanupTests,
+  createSingleServer,
+  makeGetRequest,
+  PeerTubeServer,
+  PluginsCommand,
+  setAccessTokensToServers
+} from '@shared/extra-utils'
+import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test plugin altering video constants', function () {
-  let server: ServerInfo
+  let server: PeerTubeServer
 
   before(async function () {
     this.timeout(30000)
 
-    server = await flushAndRunServer(1)
+    server = await createSingleServer(1)
     await setAccessTokensToServers([ server ])
 
-    await installPlugin({
-      url: server.url,
-      accessToken: server.accessToken,
-      path: getPluginTestPath('-video-constants')
-    })
+    await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-video-constants') })
   })
 
   it('Should have updated languages', async function () {
-    const res = await getVideoLanguages(server.url)
-    const languages = res.body
+    const languages = await server.videos.getLanguages()
 
     expect(languages['en']).to.not.exist
     expect(languages['fr']).to.not.exist
@@ -49,8 +38,7 @@ describe('Test plugin altering video constants', function () {
   })
 
   it('Should have updated categories', async function () {
-    const res = await getVideoCategories(server.url)
-    const categories = res.body
+    const categories = await server.videos.getCategories()
 
     expect(categories[1]).to.not.exist
     expect(categories[2]).to.not.exist
@@ -60,8 +48,7 @@ describe('Test plugin altering video constants', function () {
   })
 
   it('Should have updated licences', async function () {
-    const res = await getVideoLicences(server.url)
-    const licences = res.body
+    const licences = await server.videos.getLicences()
 
     expect(licences[1]).to.not.exist
     expect(licences[7]).to.not.exist
@@ -71,8 +58,7 @@ describe('Test plugin altering video constants', function () {
   })
 
   it('Should have updated video privacies', async function () {
-    const res = await getVideoPrivacies(server.url)
-    const privacies = res.body
+    const privacies = await server.videos.getPrivacies()
 
     expect(privacies[1]).to.exist
     expect(privacies[2]).to.not.exist
@@ -81,8 +67,7 @@ describe('Test plugin altering video constants', function () {
   })
 
   it('Should have updated playlist privacies', async function () {
-    const res = await getVideoPlaylistPrivacies(server.url)
-    const playlistPrivacies = res.body
+    const playlistPrivacies = await server.playlists.getPrivacies()
 
     expect(playlistPrivacies[1]).to.exist
     expect(playlistPrivacies[2]).to.exist
@@ -90,38 +75,30 @@ describe('Test plugin altering video constants', function () {
   })
 
   it('Should not be able to create a video with this privacy', async function () {
-    const attrs = { name: 'video', privacy: 2 }
-    await uploadVideo(server.url, server.accessToken, attrs, HttpStatusCode.BAD_REQUEST_400)
+    const attributes = { name: 'video', privacy: 2 }
+    await server.videos.upload({ attributes, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should not be able to create a video with this privacy', async function () {
-    const attrs = { displayName: 'video playlist', privacy: VideoPlaylistPrivacy.PRIVATE }
-    await createVideoPlaylist({
-      url: server.url,
-      token: server.accessToken,
-      playlistAttrs: attrs,
-      expectedStatus: HttpStatusCode.BAD_REQUEST_400
-    })
+    const attributes = { displayName: 'video playlist', privacy: VideoPlaylistPrivacy.PRIVATE }
+    await server.playlists.create({ attributes, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
   })
 
   it('Should be able to upload a video with these values', async function () {
-    const attrs = { name: 'video', category: 42, licence: 42, language: 'al_bhed2' }
-    const resUpload = await uploadVideo(server.url, server.accessToken, attrs)
+    const attributes = { name: 'video', category: 42, licence: 42, language: 'al_bhed2' }
+    const { uuid } = await server.videos.upload({ attributes })
 
-    const res = await getVideo(server.url, resUpload.body.video.uuid)
-
-    const video: VideoDetails = res.body
+    const video = await server.videos.get({ id: uuid })
     expect(video.language.label).to.equal('Al Bhed 2')
     expect(video.licence.label).to.equal('Best licence')
     expect(video.category.label).to.equal('Best category')
   })
 
   it('Should uninstall the plugin and reset languages, categories, licences and privacies', async function () {
-    await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-video-constants' })
+    await server.plugins.uninstall({ npmName: 'peertube-plugin-test-video-constants' })
 
     {
-      const res = await getVideoLanguages(server.url)
-      const languages = res.body
+      const languages = await server.videos.getLanguages()
 
       expect(languages['en']).to.equal('English')
       expect(languages['fr']).to.equal('French')
@@ -132,8 +109,7 @@ describe('Test plugin altering video constants', function () {
     }
 
     {
-      const res = await getVideoCategories(server.url)
-      const categories = res.body
+      const categories = await server.videos.getCategories()
 
       expect(categories[1]).to.equal('Music')
       expect(categories[2]).to.equal('Films')
@@ -143,8 +119,7 @@ describe('Test plugin altering video constants', function () {
     }
 
     {
-      const res = await getVideoLicences(server.url)
-      const licences = res.body
+      const licences = await server.videos.getLicences()
 
       expect(licences[1]).to.equal('Attribution')
       expect(licences[7]).to.equal('Public Domain Dedication')
@@ -154,8 +129,7 @@ describe('Test plugin altering video constants', function () {
     }
 
     {
-      const res = await getVideoPrivacies(server.url)
-      const privacies = res.body
+      const privacies = await server.videos.getPrivacies()
 
       expect(privacies[1]).to.exist
       expect(privacies[2]).to.exist
@@ -164,8 +138,7 @@ describe('Test plugin altering video constants', function () {
     }
 
     {
-      const res = await getVideoPlaylistPrivacies(server.url)
-      const playlistPrivacies = res.body
+      const playlistPrivacies = await server.playlists.getPrivacies()
 
       expect(playlistPrivacies[1]).to.exist
       expect(playlistPrivacies[2]).to.exist
@@ -173,6 +146,37 @@ describe('Test plugin altering video constants', function () {
     }
   })
 
+  it('Should be able to reset categories', async function () {
+    await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-video-constants') })
+
+    {
+      const categories = await server.videos.getCategories()
+
+      expect(categories[1]).to.not.exist
+      expect(categories[2]).to.not.exist
+
+      expect(categories[42]).to.exist
+      expect(categories[43]).to.exist
+    }
+
+    await makeGetRequest({
+      url: server.url,
+      token: server.accessToken,
+      path: '/plugins/test-video-constants/router/reset-categories',
+      expectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+
+    {
+      const categories = await server.videos.getCategories()
+
+      expect(categories[1]).to.exist
+      expect(categories[2]).to.exist
+
+      expect(categories[42]).to.not.exist
+      expect(categories[43]).to.not.exist
+    }
+  })
+
   after(async function () {
     await cleanupTests([ server ])
   })
index 7b94306cdbd3437342f770c070ae556c6394b5a5..52e6ea593ef662811f828efa061275118a33559b 100644 (file)
@@ -1,14 +1,11 @@
+import { Command } from 'commander'
 import { Netrc } from 'netrc-parser'
-import { getAppNumber, isTestInstance } from '../helpers/core-utils'
 import { join } from 'path'
-import { root } from '../../shared/extra-utils/miscs/miscs'
-import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels'
-import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
 import { createLogger, format, transports } from 'winston'
-import { getMyUserInformation } from '@shared/extra-utils/users/users'
-import { User, UserRole } from '@shared/models'
-import { getAccessToken } from '@shared/extra-utils/users/login'
-import { Command } from 'commander'
+import { PeerTubeServer } from '@shared/extra-utils'
+import { UserRole } from '@shared/models'
+import { VideoPrivacy } from '../../shared/models/videos'
+import { getAppNumber, isTestInstance, root } from '../helpers/core-utils'
 
 let configName = 'PeerTube/CLI'
 if (isTestInstance()) configName += `-${getAppNumber()}`
@@ -17,17 +14,16 @@ const config = require('application-config')(configName)
 
 const version = require('../../../package.json').version
 
-async function getAdminTokenOrDie (url: string, username: string, password: string) {
-  const accessToken = await getAccessToken(url, username, password)
-  const resMe = await getMyUserInformation(url, accessToken)
-  const me: User = resMe.body
+async function getAdminTokenOrDie (server: PeerTubeServer, username: string, password: string) {
+  const token = await server.login.getAccessToken(username, password)
+  const me = await server.users.getMyInfo({ token })
 
   if (me.role !== UserRole.ADMINISTRATOR) {
     console.error('You must be an administrator.')
     process.exit(-1)
   }
 
-  return accessToken
+  return token
 }
 
 interface Settings {
@@ -128,7 +124,7 @@ function buildCommonVideoOptions (command: Command) {
     .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info')
 }
 
-async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any = {}) {
+async function buildVideoAttributesFromCommander (server: PeerTubeServer, command: Command, defaultAttributes: any = {}) {
   const options = command.opts()
 
   const defaultBooleanAttributes = {
@@ -164,8 +160,7 @@ async function buildVideoAttributesFromCommander (url: string, command: Command,
   Object.assign(videoAttributes, booleanAttributes)
 
   if (options.channelName) {
-    const res = await getVideoChannel(url, options.channelName)
-    const videoChannel: VideoChannel = res.body
+    const videoChannel = await server.channels.get({ channelName: options.channelName })
 
     Object.assign(videoAttributes, { channelId: videoChannel.id })
 
@@ -184,6 +179,19 @@ function getServerCredentials (program: Command) {
                 })
 }
 
+function buildServer (url: string) {
+  return new PeerTubeServer({ url })
+}
+
+async function assignToken (server: PeerTubeServer, username: string, password: string) {
+  const bodyClient = await server.login.getClient()
+  const client = { id: bodyClient.client_id, secret: bodyClient.client_secret }
+
+  const body = await server.login.login({ client, user: { username, password } })
+
+  server.accessToken = body.access_token
+}
+
 function getLogger (logLevel = 'info') {
   const logLevels = {
     0: 0,
@@ -230,5 +238,7 @@ export {
   buildCommonVideoOptions,
   buildVideoAttributesFromCommander,
 
-  getAdminTokenOrDie
+  getAdminTokenOrDie,
+  buildServer,
+  assignToken
 }
index 1934e7986ea62bff00e8432c808340b161c37748..b9f4ef4f8ef181a2b7486e24ec294df461a1b21a 100644 (file)
@@ -5,9 +5,8 @@ registerTSPaths()
 
 import { OptionValues, program } from 'commander'
 import * as prompt from 'prompt'
-import { getNetrc, getSettings, writeSettings } from './cli'
+import { assignToken, buildServer, getNetrc, getSettings, writeSettings } from './cli'
 import { isUserUsernameValid } from '../helpers/custom-validators/users'
-import { getAccessToken } from '../../shared/extra-utils'
 import * as CliTable3 from 'cli-table3'
 
 async function delInstance (url: string) {
@@ -97,7 +96,8 @@ program
         // @see https://github.com/Chocobozzz/PeerTube/issues/3520
         result.url = stripExtraneousFromPeerTubeUrl(result.url)
 
-        await getAccessToken(result.url, result.username, result.password)
+        const server = buildServer(result.url)
+        await assignToken(server, result.username, result.password)
       } catch (err) {
         console.error(err.message)
         process.exit(-1)
index 9488eba0eb516ab48eac3a87ce786399ab6fcf14..a67de9180d3b9b1257adc2adef47dc0b728989f4 100644 (file)
@@ -2,7 +2,7 @@ import { registerTSPaths } from '../helpers/register-ts-paths'
 registerTSPaths()
 
 import { program } from 'commander'
-import { getClient, Server, serverLogin } from '../../shared/extra-utils'
+import { assignToken, buildServer } from './cli'
 
 program
   .option('-u, --url <url>', 'Server url')
@@ -24,24 +24,11 @@ if (
   process.exit(-1)
 }
 
-getClient(options.url)
-  .then(res => {
-    const server = {
-      url: options.url,
-      user: {
-        username: options.username,
-        password: options.password
-      },
-      client: {
-        id: res.body.client_id,
-        secret: res.body.client_secret
-      }
-    } as Server
+const server = buildServer(options.url)
 
-    return serverLogin(server)
-  })
-  .then(accessToken => {
-    console.log(accessToken)
+assignToken(server, options.username, options.password)
+  .then(() => {
+    console.log(server.accessToken)
     process.exit(0)
   })
   .catch(err => {
index 101a95b2ac593ff696c05f9198deedf08b250720..52aae3d2c372cd51783376ec3dd0d0ea82bde4af 100644 (file)
@@ -8,17 +8,19 @@ import { truncate } from 'lodash'
 import { join } from 'path'
 import * as prompt from 'prompt'
 import { promisify } from 'util'
-import { advancedVideosSearch, getClient, getVideoCategories, login, uploadVideo } from '../../shared/extra-utils/index'
+import { YoutubeDL } from '@server/helpers/youtube-dl'
 import { sha256 } from '../helpers/core-utils'
 import { doRequestAndSaveToFile } from '../helpers/requests'
 import { CONSTRAINTS_FIELDS } from '../initializers/constants'
-import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getLogger, getServerCredentials } from './cli'
-import { YoutubeDL } from '@server/helpers/youtube-dl'
-
-type UserInfo = {
-  username: string
-  password: string
-}
+import {
+  assignToken,
+  buildCommonVideoOptions,
+  buildServer,
+  buildVideoAttributesFromCommander,
+  getLogger,
+  getServerCredentials
+} from './cli'
+import { PeerTubeServer } from '@shared/extra-utils'
 
 const processOptions = {
   maxBuffer: Infinity
@@ -62,17 +64,13 @@ getServerCredentials(command)
     url = normalizeTargetUrl(url)
     options.targetUrl = normalizeTargetUrl(options.targetUrl)
 
-    const user = { username, password }
-
-    run(url, user)
+    run(url, username, password)
       .catch(err => exitError(err))
   })
   .catch(err => console.error(err))
 
-async function run (url: string, user: UserInfo) {
-  if (!user.password) {
-    user.password = await promptPassword()
-  }
+async function run (url: string, username: string, password: string) {
+  if (!password) password = await promptPassword()
 
   const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL()
 
@@ -111,7 +109,8 @@ async function run (url: string, user: UserInfo) {
       await processVideo({
         cwd: options.tmpdir,
         url,
-        user,
+        username,
+        password,
         youtubeInfo: info
       })
     } catch (err) {
@@ -119,17 +118,18 @@ async function run (url: string, user: UserInfo) {
     }
   }
 
-  log.info('Video/s for user %s imported: %s', user.username, options.targetUrl)
+  log.info('Video/s for user %s imported: %s', username, options.targetUrl)
   process.exit(0)
 }
 
 async function processVideo (parameters: {
   cwd: string
   url: string
-  user: { username: string, password: string }
+  username: string
+  password: string
   youtubeInfo: any
 }) {
-  const { youtubeInfo, cwd, url, user } = parameters
+  const { youtubeInfo, cwd, url, username, password } = parameters
   const youtubeDL = new YoutubeDL('', [])
 
   log.debug('Fetching object.', youtubeInfo)
@@ -138,22 +138,29 @@ async function processVideo (parameters: {
   log.debug('Fetched object.', videoInfo)
 
   const originallyPublishedAt = youtubeDL.buildOriginallyPublishedAt(videoInfo)
+
   if (options.since && originallyPublishedAt && originallyPublishedAt.getTime() < options.since.getTime()) {
-    log.info('Video "%s" has been published before "%s", don\'t upload it.\n',
-      videoInfo.title, formatDate(options.since))
+    log.info('Video "%s" has been published before "%s", don\'t upload it.\n', videoInfo.title, formatDate(options.since))
     return
   }
+
   if (options.until && originallyPublishedAt && originallyPublishedAt.getTime() > options.until.getTime()) {
-    log.info('Video "%s" has been published after "%s", don\'t upload it.\n',
-      videoInfo.title, formatDate(options.until))
+    log.info('Video "%s" has been published after "%s", don\'t upload it.\n', videoInfo.title, formatDate(options.until))
     return
   }
 
-  const result = await advancedVideosSearch(url, { search: videoInfo.title, sort: '-match', searchTarget: 'local' })
+  const server = buildServer(url)
+  const { data } = await server.search.advancedVideoSearch({
+    search: {
+      search: videoInfo.title,
+      sort: '-match',
+      searchTarget: 'local'
+    }
+  })
 
   log.info('############################################################\n')
 
-  if (result.body.data.find(v => v.name === videoInfo.title)) {
+  if (data.find(v => v.name === videoInfo.title)) {
     log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title)
     return
   }
@@ -172,7 +179,8 @@ async function processVideo (parameters: {
       youtubeDL,
       cwd,
       url,
-      user,
+      username,
+      password,
       videoInfo: normalizeObject(videoInfo),
       videoPath: path
     })
@@ -187,11 +195,15 @@ async function uploadVideoOnPeerTube (parameters: {
   videoPath: string
   cwd: string
   url: string
-  user: { username: string, password: string }
+  username: string
+  password: string
 }) {
-  const { youtubeDL, videoInfo, videoPath, cwd, url, user } = parameters
+  const { youtubeDL, videoInfo, videoPath, cwd, url, username, password } = parameters
 
-  const category = await getCategory(videoInfo.categories, url)
+  const server = buildServer(url)
+  await assignToken(server, username, password)
+
+  const category = await getCategory(server, videoInfo.categories)
   const licence = getLicence(videoInfo.license)
   let tags = []
   if (Array.isArray(videoInfo.tags)) {
@@ -223,28 +235,28 @@ async function uploadVideoOnPeerTube (parameters: {
     tags
   }
 
-  const videoAttributes = await buildVideoAttributesFromCommander(url, program, defaultAttributes)
+  const baseAttributes = await buildVideoAttributesFromCommander(server, program, defaultAttributes)
+
+  const attributes = {
+    ...baseAttributes,
 
-  Object.assign(videoAttributes, {
     originallyPublishedAt: originallyPublishedAt ? originallyPublishedAt.toISOString() : null,
     thumbnailfile,
     previewfile: thumbnailfile,
     fixture: videoPath
-  })
-
-  log.info('\nUploading on PeerTube video "%s".', videoAttributes.name)
+  }
 
-  let accessToken = await getAccessTokenOrDie(url, user)
+  log.info('\nUploading on PeerTube video "%s".', attributes.name)
 
   try {
-    await uploadVideo(url, accessToken, videoAttributes)
+    await server.videos.upload({ attributes })
   } catch (err) {
     if (err.message.indexOf('401') !== -1) {
       log.info('Got 401 Unauthorized, token may have expired, renewing token and retry.')
 
-      accessToken = await getAccessTokenOrDie(url, user)
+      server.accessToken = await server.login.getAccessToken(username, password)
 
-      await uploadVideo(url, accessToken, videoAttributes)
+      await server.videos.upload({ attributes })
     } else {
       exitError(err.message)
     }
@@ -253,20 +265,19 @@ async function uploadVideoOnPeerTube (parameters: {
   await remove(videoPath)
   if (thumbnailfile) await remove(thumbnailfile)
 
-  log.warn('Uploaded video "%s"!\n', videoAttributes.name)
+  log.warn('Uploaded video "%s"!\n', attributes.name)
 }
 
 /* ---------------------------------------------------------- */
 
-async function getCategory (categories: string[], url: string) {
+async function getCategory (server: PeerTubeServer, categories: string[]) {
   if (!categories) return undefined
 
   const categoryString = categories[0]
 
   if (categoryString === 'News & Politics') return 11
 
-  const res = await getVideoCategories(url)
-  const categoriesServer = res.body
+  const categoriesServer = await server.videos.getCategories()
 
   for (const key of Object.keys(categoriesServer)) {
     const categoryServer = categoriesServer[key]
@@ -362,21 +373,6 @@ async function promptPassword () {
   })
 }
 
-async function getAccessTokenOrDie (url: string, user: UserInfo) {
-  const resClient = await getClient(url)
-  const client = {
-    id: resClient.body.client_id,
-    secret: resClient.body.client_secret
-  }
-
-  try {
-    const res = await login(url, client, user)
-    return res.body.access_token
-  } catch (err) {
-    exitError('Cannot authenticate. Please check your username/password.')
-  }
-}
-
 function parseDate (dateAsStr: string): Date {
   if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) {
     exitError(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`)
index 54ea1264d29a637f7a538d92e3f1f4fd9e487da7..d9c285115cdab926086e048dd0b1d308fa62fa0b 100644 (file)
@@ -4,9 +4,8 @@ import { registerTSPaths } from '../helpers/register-ts-paths'
 registerTSPaths()
 
 import { program, Command, OptionValues } from 'commander'
-import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins'
-import { getAdminTokenOrDie, getServerCredentials } from './cli'
-import { PeerTubePlugin, PluginType } from '../../shared/models'
+import { assignToken, buildServer, getServerCredentials } from './cli'
+import { PluginType } from '../../shared/models'
 import { isAbsolute } from 'path'
 import * as CliTable3 from 'cli-table3'
 
@@ -63,28 +62,21 @@ program.parse(process.argv)
 
 async function pluginsListCLI (command: Command, options: OptionValues) {
   const { url, username, password } = await getServerCredentials(command)
-  const accessToken = await getAdminTokenOrDie(url, username, password)
+  const server = buildServer(url)
+  await assignToken(server, username, password)
 
   let pluginType: PluginType
   if (options.onlyThemes) pluginType = PluginType.THEME
   if (options.onlyPlugins) pluginType = PluginType.PLUGIN
 
-  const res = await listPlugins({
-    url,
-    accessToken,
-    start: 0,
-    count: 100,
-    sort: 'name',
-    pluginType
-  })
-  const plugins: PeerTubePlugin[] = res.body.data
+  const { data } = await server.plugins.list({ start: 0, count: 100, sort: 'name', pluginType })
 
   const table = new CliTable3({
     head: [ 'name', 'version', 'homepage' ],
     colWidths: [ 50, 10, 50 ]
   }) as any
 
-  for (const plugin of plugins) {
+  for (const plugin of data) {
     const npmName = plugin.type === PluginType.PLUGIN
       ? 'peertube-plugin-' + plugin.name
       : 'peertube-theme-' + plugin.name
@@ -113,15 +105,11 @@ async function installPluginCLI (command: Command, options: OptionValues) {
   }
 
   const { url, username, password } = await getServerCredentials(command)
-  const accessToken = await getAdminTokenOrDie(url, username, password)
+  const server = buildServer(url)
+  await assignToken(server, username, password)
 
   try {
-    await installPlugin({
-      url,
-      accessToken,
-      npmName: options.npmName,
-      path: options.path
-    })
+    await server.plugins.install({ npmName: options.npmName, path: options.path })
   } catch (err) {
     console.error('Cannot install plugin.', err)
     process.exit(-1)
@@ -144,15 +132,11 @@ async function updatePluginCLI (command: Command, options: OptionValues) {
   }
 
   const { url, username, password } = await getServerCredentials(command)
-  const accessToken = await getAdminTokenOrDie(url, username, password)
+  const server = buildServer(url)
+  await assignToken(server, username, password)
 
   try {
-    await updatePlugin({
-      url,
-      accessToken,
-      npmName: options.npmName,
-      path: options.path
-    })
+    await server.plugins.update({ npmName: options.npmName, path: options.path })
   } catch (err) {
     console.error('Cannot update plugin.', err)
     process.exit(-1)
@@ -170,14 +154,11 @@ async function uninstallPluginCLI (command: Command, options: OptionValues) {
   }
 
   const { url, username, password } = await getServerCredentials(command)
-  const accessToken = await getAdminTokenOrDie(url, username, password)
+  const server = buildServer(url)
+  await assignToken(server, username, password)
 
   try {
-    await uninstallPlugin({
-      url,
-      accessToken,
-      npmName: options.npmName
-    })
+    await server.plugins.uninstall({ npmName: options.npmName })
   } catch (err) {
     console.error('Cannot uninstall plugin.', err)
     process.exit(-1)
index 4810deee02caf5f0558fcc5d03a533f1015d5668..73b026ac8f40e6f941733330fd5ccfe3eeb69aa3 100644 (file)
@@ -1,17 +1,13 @@
-// eslint-disable @typescript-eslint/no-unnecessary-type-assertion
-
 import { registerTSPaths } from '../helpers/register-ts-paths'
 registerTSPaths()
 
-import { program, Command } from 'commander'
-import { getAdminTokenOrDie, getServerCredentials } from './cli'
-import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
-import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy } from '@shared/extra-utils/server/redundancy'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import validator from 'validator'
 import * as CliTable3 from 'cli-table3'
-import { URL } from 'url'
+import { Command, program } from 'commander'
 import { uniq } from 'lodash'
+import { URL } from 'url'
+import validator from 'validator'
+import { HttpStatusCode, VideoRedundanciesTarget } from '@shared/models'
+import { assignToken, buildServer, getServerCredentials } from './cli'
 
 import bytes = require('bytes')
 
@@ -63,15 +59,16 @@ program.parse(process.argv)
 
 async function listRedundanciesCLI (target: VideoRedundanciesTarget) {
   const { url, username, password } = await getServerCredentials(program)
-  const accessToken = await getAdminTokenOrDie(url, username, password)
+  const server = buildServer(url)
+  await assignToken(server, username, password)
 
-  const redundancies = await listVideoRedundanciesData(url, accessToken, target)
+  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 redundancies) {
+  for (const redundancy of data) {
     const webtorrentFiles = redundancy.redundancies.files
     const streamingPlaylists = redundancy.redundancies.streamingPlaylists
 
@@ -106,7 +103,8 @@ async function listRedundanciesCLI (target: VideoRedundanciesTarget) {
 
 async function addRedundancyCLI (options: { video: number }, command: Command) {
   const { url, username, password } = await getServerCredentials(command)
-  const accessToken = await getAdminTokenOrDie(url, username, password)
+  const server = buildServer(url)
+  await assignToken(server, username, password)
 
   if (!options.video || validator.isInt('' + options.video) === false) {
     console.error('You need to specify the video id to duplicate and it should be a number.\n')
@@ -115,11 +113,7 @@ async function addRedundancyCLI (options: { video: number }, command: Command) {
   }
 
   try {
-    await addVideoRedundancy({
-      url,
-      accessToken,
-      videoId: options.video
-    })
+    await server.redundancy.addVideo({ videoId: options.video })
 
     console.log('Video will be duplicated by your instance!')
 
@@ -139,7 +133,8 @@ async function addRedundancyCLI (options: { video: number }, command: Command) {
 
 async function removeRedundancyCLI (options: { video: number }, command: Command) {
   const { url, username, password } = await getServerCredentials(command)
-  const accessToken = await getAdminTokenOrDie(url, username, password)
+  const server = buildServer(url)
+  await assignToken(server, username, password)
 
   if (!options.video || validator.isInt('' + options.video) === false) {
     console.error('You need to specify the video id to remove from your redundancies.\n')
@@ -149,12 +144,12 @@ async function removeRedundancyCLI (options: { video: number }, command: Command
 
   const videoId = parseInt(options.video + '', 10)
 
-  let redundancies = await listVideoRedundanciesData(url, accessToken, 'my-videos')
-  let videoRedundancy = redundancies.find(r => videoId === r.id)
+  const myVideoRedundancies = await server.redundancy.listVideos({ target: 'my-videos' })
+  let videoRedundancy = myVideoRedundancies.data.find(r => videoId === r.id)
 
   if (!videoRedundancy) {
-    redundancies = await listVideoRedundanciesData(url, accessToken, 'remote-videos')
-    videoRedundancy = redundancies.find(r => videoId === r.id)
+    const remoteVideoRedundancies = await server.redundancy.listVideos({ target: 'remote-videos' })
+    videoRedundancy = remoteVideoRedundancies.data.find(r => videoId === r.id)
   }
 
   if (!videoRedundancy) {
@@ -168,11 +163,7 @@ async function removeRedundancyCLI (options: { video: number }, command: Command
                                .map(r => r.id)
 
     for (const id of ids) {
-      await removeVideoRedundancy({
-        url,
-        accessToken,
-        redundancyId: id
-      })
+      await server.redundancy.removeVideo({ redundancyId: id })
     }
 
     console.log('Video redundancy removed!')
@@ -183,16 +174,3 @@ async function removeRedundancyCLI (options: { video: number }, command: Command
     process.exit(-1)
   }
 }
-
-async function listVideoRedundanciesData (url: string, accessToken: string, target: VideoRedundanciesTarget) {
-  const res = await listVideoRedundancies({
-    url,
-    accessToken,
-    start: 0,
-    count: 100,
-    sort: 'name',
-    target
-  })
-
-  return res.body.data as VideoRedundancy[]
-}
index 02edbd809296e3ec7d4d875e3957b573af22eefc..01fb1fe8d5e3e76151f91ed62426d94a029d0eb6 100644 (file)
@@ -4,9 +4,7 @@ registerTSPaths()
 import { program } from 'commander'
 import { access, constants } from 'fs-extra'
 import { isAbsolute } from 'path'
-import { getAccessToken } from '../../shared/extra-utils'
-import { uploadVideo } from '../../shared/extra-utils/'
-import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials } from './cli'
+import { assignToken, buildCommonVideoOptions, buildServer, buildVideoAttributesFromCommander, getServerCredentials } from './cli'
 
 let command = program
   .name('upload')
@@ -46,22 +44,25 @@ getServerCredentials(command)
   .catch(err => console.error(err))
 
 async function run (url: string, username: string, password: string) {
-  const accessToken = await getAccessToken(url, username, password)
+  const server = buildServer(url)
+  await assignToken(server, username, password)
 
   await access(options.file, constants.F_OK)
 
   console.log('Uploading %s video...', options.videoName)
 
-  const videoAttributes = await buildVideoAttributesFromCommander(url, program)
+  const baseAttributes = await buildVideoAttributesFromCommander(server, program)
+
+  const attributes = {
+    ...baseAttributes,
 
-  Object.assign(videoAttributes, {
     fixture: options.file,
     thumbnailfile: options.thumbnail,
     previewfile: options.preview
-  })
+  }
 
   try {
-    await uploadVideo(url, accessToken, videoAttributes)
+    await server.videos.upload({ attributes })
     console.log(`Video ${options.videoName} uploaded.`)
     process.exit(0)
   } catch (err) {
similarity index 59%
rename from server/tools/test.ts
rename to server/tools/test-live.ts
index fbdbae0b0860495278c9f5a33ee5849daa725ace..50dc04438f5764f402ed8a383cdb3139afbd5397 100644 (file)
@@ -1,26 +1,23 @@
-import { registerTSPaths } from '../helpers/register-ts-paths'
-registerTSPaths()
-
-import { LiveVideo, LiveVideoCreate, VideoPrivacy } from '@shared/models'
 import { program } from 'commander'
+import { LiveVideoCreate, VideoPrivacy } from '@shared/models'
 import {
-  createLive,
-  flushAndRunServer,
-  getLive,
+  createSingleServer,
   killallServers,
   sendRTMPStream,
-  ServerInfo,
+  PeerTubeServer,
   setAccessTokensToServers,
-  setDefaultVideoChannel,
-  updateCustomSubConfig
+  setDefaultVideoChannel
 } from '../../shared/extra-utils'
+import { registerTSPaths } from '../helpers/register-ts-paths'
+
+registerTSPaths()
 
 type CommandType = 'live-mux' | 'live-transcoding'
 
 registerTSPaths()
 
 const command = program
-  .name('test')
+  .name('test-live')
   .option('-t, --type <type>', 'live-muxing|live-transcoding')
   .parse(process.argv)
 
@@ -39,11 +36,11 @@ async function run () {
 
   console.log('Starting server.')
 
-  const server = await flushAndRunServer(1, {}, [], { hideLogs: false, execArgv: [ '--inspect' ] })
+  const server = await createSingleServer(1, {}, { hideLogs: false, nodeArgs: [ '--inspect' ] })
 
-  const cleanup = () => {
+  const cleanup = async () => {
     console.log('Killing server')
-    killallServers([ server ])
+    await killallServers([ server ])
   }
 
   process.on('exit', cleanup)
@@ -57,17 +54,15 @@ async function run () {
   const attributes: LiveVideoCreate = {
     name: 'live',
     saveReplay: true,
-    channelId: server.videoChannel.id,
+    channelId: server.store.channel.id,
     privacy: VideoPrivacy.PUBLIC
   }
 
   console.log('Creating live.')
 
-  const res = await createLive(server.url, server.accessToken, attributes)
-  const liveVideoUUID = res.body.video.uuid
+  const { uuid: liveVideoUUID } = await server.live.create({ fields: attributes })
 
-  const resLive = await getLive(server.url, server.accessToken, liveVideoUUID)
-  const live: LiveVideo = resLive.body
+  const live = await server.live.get({ videoId: liveVideoUUID })
 
   console.log('Sending RTMP stream.')
 
@@ -86,19 +81,21 @@ async function run () {
 
 // ----------------------------------------------------------------------------
 
-async function buildConfig (server: ServerInfo, commandType: CommandType) {
-  await updateCustomSubConfig(server.url, server.accessToken, {
-    instance: {
-      customizations: {
-        javascript: '',
-        css: ''
-      }
-    },
-    live: {
-      enabled: true,
-      allowReplay: true,
-      transcoding: {
-        enabled: commandType === 'live-transcoding'
+async function buildConfig (server: PeerTubeServer, commandType: CommandType) {
+  await server.config.updateCustomSubConfig({
+    newConfig: {
+      instance: {
+        customizations: {
+          javascript: '',
+          css: ''
+        }
+      },
+      live: {
+        enabled: true,
+        allowReplay: true,
+        transcoding: {
+          enabled: commandType === 'live-transcoding'
+        }
       }
     }
   })
index 8b3ef51fc625b6d7fffa21e93bb050617fcebcbe..1e4dccb8e99c839c3d5bd92049d0140c9bbf83aa 100644 (file)
@@ -39,5 +39,5 @@ export type MStreamingPlaylistRedundanciesOpt =
   PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]>
 
 export function isStreamingPlaylist (value: MVideo | MStreamingPlaylistVideo): value is MStreamingPlaylistVideo {
-  return !!(value as MStreamingPlaylist).playlistUrl
+  return !!(value as MStreamingPlaylist).videoId
 }
index 1a8dc343097afd4866e67d9bec14676f01cd3117..1a99b598aabacb6be95983d07e610c81ff5a726c 100644 (file)
@@ -1,4 +1,5 @@
 
+import { OutgoingHttpHeaders } from 'http'
 import { RegisterServerAuthExternalOptions } from '@server/types'
 import {
   MAbuseMessage,
@@ -22,8 +23,7 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server'
 import { MVideoImportDefault } from '@server/types/models/video/video-import'
 import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
 import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
-import { HttpMethod } from '@shared/core-utils/miscs/http-methods'
-import { PeerTubeProblemDocumentData, ServerErrorCode, VideoCreate } from '@shared/models'
+import { HttpMethod, PeerTubeProblemDocumentData, ServerErrorCode, VideoCreate } from '@shared/models'
 import { File as UploadXFile, Metadata } from '@uploadx/core'
 import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
 import {
@@ -41,6 +41,7 @@ import {
   MVideoShareActor,
   MVideoThumbnail
 } from '../../types/models'
+import { Writable } from 'stream'
 
 declare module 'express' {
   export interface Request {
@@ -99,6 +100,15 @@ declare module 'express' {
     }) => void
 
     locals: {
+      apicache: {
+        content: string | Buffer
+        write: Writable['write']
+        writeHead: Response['writeHead']
+        end: Response['end']
+        cacheable: boolean
+        headers: OutgoingHttpHeaders
+      }
+
       docUrl?: string
 
       videoAPI?: MVideoFormattableDetails
similarity index 53%
rename from shared/core-utils/miscs/date.ts
rename to shared/core-utils/common/date.ts
index 4f92f758ff665600d5b392d614e4366c0b18522a..3e4a3c08c7e03c366997588431169dd4a719bf37 100644 (file)
@@ -43,6 +43,49 @@ function isLastWeek (d: Date) {
   return getDaysDifferences(now, d) <= 7
 }
 
+function timeToInt (time: number | string) {
+  if (!time) return 0
+  if (typeof time === 'number') return time
+
+  const reg = /^((\d+)[h:])?((\d+)[m:])?((\d+)s?)?$/
+  const matches = time.match(reg)
+
+  if (!matches) return 0
+
+  const hours = parseInt(matches[2] || '0', 10)
+  const minutes = parseInt(matches[4] || '0', 10)
+  const seconds = parseInt(matches[6] || '0', 10)
+
+  return hours * 3600 + minutes * 60 + seconds
+}
+
+function secondsToTime (seconds: number, full = false, symbol?: string) {
+  let time = ''
+
+  if (seconds === 0 && !full) return '0s'
+
+  const hourSymbol = (symbol || 'h')
+  const minuteSymbol = (symbol || 'm')
+  const secondsSymbol = full ? '' : 's'
+
+  const hours = Math.floor(seconds / 3600)
+  if (hours >= 1) time = hours + hourSymbol
+  else if (full) time = '0' + hourSymbol
+
+  seconds %= 3600
+  const minutes = Math.floor(seconds / 60)
+  if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol
+  else if (minutes >= 1) time += minutes + minuteSymbol
+  else if (full) time += '00' + minuteSymbol
+
+  seconds %= 60
+  if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol
+  else if (seconds >= 1) time += seconds + secondsSymbol
+  else if (full) time += '00'
+
+  return time
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -51,7 +94,9 @@ export {
   isThisMonth,
   isToday,
   isLastMonth,
-  isLastWeek
+  isLastWeek,
+  timeToInt,
+  secondsToTime
 }
 
 // ---------------------------------------------------------------------------
diff --git a/shared/core-utils/common/index.ts b/shared/core-utils/common/index.ts
new file mode 100644 (file)
index 0000000..0908ff9
--- /dev/null
@@ -0,0 +1,6 @@
+export * from './date'
+export * from './miscs'
+export * from './regexp'
+export * from './promises'
+export * from './types'
+export * from './url'
similarity index 81%
rename from shared/core-utils/miscs/miscs.ts
rename to shared/core-utils/common/miscs.ts
index 4780ca922d9d615843493b89a780092f2976a650..bc65dc33863ebcf111d18e05b3a0f47287ce9473 100644 (file)
@@ -20,14 +20,6 @@ function compareSemVer (a: string, b: string) {
   return segmentsA.length - segmentsB.length
 }
 
-function isPromise (value: any) {
-  return value && typeof value.then === 'function'
-}
-
-function isCatchable (value: any) {
-  return value && typeof value.catch === 'function'
-}
-
 function sortObjectComparator (key: string, order: 'asc' | 'desc') {
   return (a: any, b: any) => {
     if (a[key] < b[key]) {
@@ -45,7 +37,5 @@ function sortObjectComparator (key: string, order: 'asc' | 'desc') {
 export {
   randomInt,
   compareSemVer,
-  isPromise,
-  isCatchable,
   sortObjectComparator
 }
diff --git a/shared/core-utils/common/promises.ts b/shared/core-utils/common/promises.ts
new file mode 100644 (file)
index 0000000..7ef9d60
--- /dev/null
@@ -0,0 +1,12 @@
+function isPromise (value: any) {
+  return value && typeof value.then === 'function'
+}
+
+function isCatchable (value: any) {
+  return value && typeof value.catch === 'function'
+}
+
+export {
+  isPromise,
+  isCatchable
+}
diff --git a/shared/core-utils/common/regexp.ts b/shared/core-utils/common/regexp.ts
new file mode 100644 (file)
index 0000000..59eb87e
--- /dev/null
@@ -0,0 +1,5 @@
+export const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
+
+export function removeFragmentedMP4Ext (path: string) {
+  return path.replace(/-fragmented.mp4$/i, '')
+}
diff --git a/shared/core-utils/common/url.ts b/shared/core-utils/common/url.ts
new file mode 100644 (file)
index 0000000..52ed247
--- /dev/null
@@ -0,0 +1,130 @@
+import { Video, VideoPlaylist } from '../../models'
+import { secondsToTime } from './date'
+
+function buildPlaylistLink (playlist: Pick<VideoPlaylist, 'shortUUID'>, base?: string) {
+  return (base ?? window.location.origin) + buildPlaylistWatchPath(playlist)
+}
+
+function buildPlaylistWatchPath (playlist: Pick<VideoPlaylist, 'shortUUID'>) {
+  return '/w/p/' + playlist.shortUUID
+}
+
+function buildVideoWatchPath (video: Pick<Video, 'shortUUID'>) {
+  return '/w/' + video.shortUUID
+}
+
+function buildVideoLink (video: Pick<Video, 'shortUUID'>, base?: string) {
+  return (base ?? window.location.origin) + buildVideoWatchPath(video)
+}
+
+function buildPlaylistEmbedPath (playlist: Pick<VideoPlaylist, 'uuid'>) {
+  return '/video-playlists/embed/' + playlist.uuid
+}
+
+function buildPlaylistEmbedLink (playlist: Pick<VideoPlaylist, 'uuid'>, base?: string) {
+  return (base ?? window.location.origin) + buildPlaylistEmbedPath(playlist)
+}
+
+function buildVideoEmbedPath (video: Pick<Video, 'uuid'>) {
+  return '/videos/embed/' + video.uuid
+}
+
+function buildVideoEmbedLink (video: Pick<Video, 'uuid'>, base?: string) {
+  return (base ?? window.location.origin) + buildVideoEmbedPath(video)
+}
+
+function decorateVideoLink (options: {
+  url: string
+
+  startTime?: number
+  stopTime?: number
+
+  subtitle?: string
+
+  loop?: boolean
+  autoplay?: boolean
+  muted?: boolean
+
+  // Embed options
+  title?: boolean
+  warningTitle?: boolean
+  controls?: boolean
+  peertubeLink?: boolean
+}) {
+  const { url } = options
+
+  const params = generateParams(window.location.search)
+
+  if (options.startTime !== undefined && options.startTime !== null) {
+    const startTimeInt = Math.floor(options.startTime)
+    params.set('start', secondsToTime(startTimeInt))
+  }
+
+  if (options.stopTime) {
+    const stopTimeInt = Math.floor(options.stopTime)
+    params.set('stop', secondsToTime(stopTimeInt))
+  }
+
+  if (options.subtitle) params.set('subtitle', options.subtitle)
+
+  if (options.loop === true) params.set('loop', '1')
+  if (options.autoplay === true) params.set('autoplay', '1')
+  if (options.muted === true) params.set('muted', '1')
+  if (options.title === false) params.set('title', '0')
+  if (options.warningTitle === false) params.set('warningTitle', '0')
+  if (options.controls === false) params.set('controls', '0')
+  if (options.peertubeLink === false) params.set('peertubeLink', '0')
+
+  return buildUrl(url, params)
+}
+
+function decoratePlaylistLink (options: {
+  url: string
+
+  playlistPosition?: number
+}) {
+  const { url } = options
+
+  const params = generateParams(window.location.search)
+
+  if (options.playlistPosition) params.set('playlistPosition', '' + options.playlistPosition)
+
+  return buildUrl(url, params)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  buildPlaylistLink,
+  buildVideoLink,
+
+  buildVideoWatchPath,
+  buildPlaylistWatchPath,
+
+  buildPlaylistEmbedPath,
+  buildVideoEmbedPath,
+
+  buildPlaylistEmbedLink,
+  buildVideoEmbedLink,
+
+  decorateVideoLink,
+  decoratePlaylistLink
+}
+
+function buildUrl (url: string, params: URLSearchParams) {
+  let hasParams = false
+  params.forEach(() => { hasParams = true })
+
+  if (hasParams) return url + '?' + params.toString()
+
+  return url
+}
+
+function generateParams (url: string) {
+  const params = new URLSearchParams(window.location.search)
+  // Unused parameters in embed
+  params.delete('videoId')
+  params.delete('resume')
+
+  return params
+}
index 42d7cab1d128143509a89d07888ebe85874eb96c..2a7d4d9828d5757d9d079b7cc4246dd609770feb 100644 (file)
@@ -1,7 +1,7 @@
 export * from './abuse'
+export * from './common'
 export * from './i18n'
-export * from './logs'
-export * from './miscs'
 export * from './plugins'
 export * from './renderer'
 export * from './users'
+export * from './utils'
diff --git a/shared/core-utils/logs/index.ts b/shared/core-utils/logs/index.ts
deleted file mode 100644 (file)
index ceb5d7a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export * from './logs'
diff --git a/shared/core-utils/logs/logs.ts b/shared/core-utils/logs/logs.ts
deleted file mode 100644 (file)
index d0996cf..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import { stat } from 'fs-extra'
-
-async function mtimeSortFilesDesc (files: string[], basePath: string) {
-  const promises = []
-  const out: { file: string, mtime: number }[] = []
-
-  for (const file of files) {
-    const p = stat(basePath + '/' + file)
-      .then(stats => {
-        if (stats.isFile()) out.push({ file, mtime: stats.mtime.getTime() })
-      })
-
-    promises.push(p)
-  }
-
-  await Promise.all(promises)
-
-  out.sort((a, b) => b.mtime - a.mtime)
-
-  return out
-}
-
-export {
-  mtimeSortFilesDesc
-}
diff --git a/shared/core-utils/miscs/index.ts b/shared/core-utils/miscs/index.ts
deleted file mode 100644 (file)
index 251df1d..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-export * from './date'
-export * from './miscs'
-export * from './types'
-export * from './http-error-codes'
-export * from './http-methods'
index 5405e0529ad0eeab00bc08ed6cee44d84a8b5ef6..92cb5ad68d0543ded523a79de2db51410445cbdd 100644 (file)
@@ -1,5 +1,5 @@
 import { HookType } from '../../models/plugins/hook-type.enum'
-import { isCatchable, isPromise } from '../miscs/miscs'
+import { isCatchable, isPromise } from '../common/promises'
 
 function getHookType (hookName: string) {
   if (hookName.startsWith('filter:')) return HookType.FILTER
diff --git a/shared/core-utils/utils/index.ts b/shared/core-utils/utils/index.ts
new file mode 100644 (file)
index 0000000..a71977d
--- /dev/null
@@ -0,0 +1 @@
+export * from './object'
diff --git a/shared/core-utils/utils/object.ts b/shared/core-utils/utils/object.ts
new file mode 100644 (file)
index 0000000..9a8a98f
--- /dev/null
@@ -0,0 +1,15 @@
+function pick <O extends object, K extends keyof O> (object: O, keys: K[]): Pick<O, K> {
+  const result: any = {}
+
+  for (const key of keys) {
+    if (Object.prototype.hasOwnProperty.call(object, key)) {
+      result[key] = object[key]
+    }
+  }
+
+  return result
+}
+
+export {
+  pick
+}
diff --git a/shared/extra-utils/bulk/bulk-command.ts b/shared/extra-utils/bulk/bulk-command.ts
new file mode 100644 (file)
index 0000000..b5c5673
--- /dev/null
@@ -0,0 +1,20 @@
+import { BulkRemoveCommentsOfBody, HttpStatusCode } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class BulkCommand extends AbstractCommand {
+
+  removeCommentsOf (options: OverrideCommandOptions & {
+    attributes: BulkRemoveCommentsOfBody
+  }) {
+    const { attributes } = options
+
+    return this.postBodyRequest({
+      ...options,
+
+      path: '/api/v1/bulk/remove-comments-of',
+      fields: attributes,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/bulk/bulk.ts b/shared/extra-utils/bulk/bulk.ts
deleted file mode 100644 (file)
index b6f437b..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BulkRemoveCommentsOfBody } from "@shared/models/bulk/bulk-remove-comments-of-body.model"
-import { makePostBodyRequest } from "../requests/requests"
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function bulkRemoveCommentsOf (options: {
-  url: string
-  token: string
-  attributes: BulkRemoveCommentsOfBody
-  expectedStatus?: number
-}) {
-  const { url, token, attributes, expectedStatus } = options
-  const path = '/api/v1/bulk/remove-comments-of'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    fields: attributes,
-    statusCodeExpected: expectedStatus || HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-export {
-  bulkRemoveCommentsOf
-}
diff --git a/shared/extra-utils/bulk/index.ts b/shared/extra-utils/bulk/index.ts
new file mode 100644 (file)
index 0000000..3915972
--- /dev/null
@@ -0,0 +1 @@
+export * from './bulk-command'
diff --git a/shared/extra-utils/cli/cli-command.ts b/shared/extra-utils/cli/cli-command.ts
new file mode 100644 (file)
index 0000000..bc1dddc
--- /dev/null
@@ -0,0 +1,23 @@
+import { exec } from 'child_process'
+import { AbstractCommand } from '../shared'
+
+export class CLICommand extends AbstractCommand {
+
+  static exec (command: string) {
+    return new Promise<string>((res, rej) => {
+      exec(command, (err, stdout, _stderr) => {
+        if (err) return rej(err)
+
+        return res(stdout)
+      })
+    })
+  }
+
+  getEnv () {
+    return `NODE_ENV=test NODE_APP_INSTANCE=${this.server.internalServerNumber}`
+  }
+
+  async execWithEnv (command: string) {
+    return CLICommand.exec(`${this.getEnv()} ${command}`)
+  }
+}
diff --git a/shared/extra-utils/cli/cli.ts b/shared/extra-utils/cli/cli.ts
deleted file mode 100644 (file)
index c62e170..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-import { exec } from 'child_process'
-
-import { ServerInfo } from '../server/servers'
-
-function getEnvCli (server?: ServerInfo) {
-  return `NODE_ENV=test NODE_APP_INSTANCE=${server.internalServerNumber}`
-}
-
-async function execCLI (command: string) {
-  return new Promise<string>((res, rej) => {
-    exec(command, (err, stdout, stderr) => {
-      if (err) return rej(err)
-
-      return res(stdout)
-    })
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  execCLI,
-  getEnvCli
-}
diff --git a/shared/extra-utils/cli/index.ts b/shared/extra-utils/cli/index.ts
new file mode 100644 (file)
index 0000000..91b5abf
--- /dev/null
@@ -0,0 +1 @@
+export * from './cli-command'
diff --git a/shared/extra-utils/custom-pages/custom-pages-command.ts b/shared/extra-utils/custom-pages/custom-pages-command.ts
new file mode 100644 (file)
index 0000000..cd869a8
--- /dev/null
@@ -0,0 +1,33 @@
+import { CustomPage, HttpStatusCode } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class CustomPagesCommand extends AbstractCommand {
+
+  getInstanceHomepage (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/custom-pages/homepage/instance'
+
+    return this.getRequestBody<CustomPage>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  updateInstanceHomepage (options: OverrideCommandOptions & {
+    content: string
+  }) {
+    const { content } = options
+    const path = '/api/v1/custom-pages/homepage/instance'
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: { content },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/custom-pages/custom-pages.ts b/shared/extra-utils/custom-pages/custom-pages.ts
deleted file mode 100644 (file)
index bf2d16c..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-import { makeGetRequest, makePutBodyRequest } from '../requests/requests'
-
-function getInstanceHomepage (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/custom-pages/homepage/instance'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected
-  })
-}
-
-function updateInstanceHomepage (url: string, token: string, content: string) {
-  const path = '/api/v1/custom-pages/homepage/instance'
-
-  return makePutBodyRequest({
-    url,
-    path,
-    token,
-    fields: { content },
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getInstanceHomepage,
-  updateInstanceHomepage
-}
diff --git a/shared/extra-utils/custom-pages/index.ts b/shared/extra-utils/custom-pages/index.ts
new file mode 100644 (file)
index 0000000..58aed04
--- /dev/null
@@ -0,0 +1 @@
+export * from './custom-pages-command'
diff --git a/shared/extra-utils/feeds/feeds-command.ts b/shared/extra-utils/feeds/feeds-command.ts
new file mode 100644 (file)
index 0000000..3c95f95
--- /dev/null
@@ -0,0 +1,44 @@
+
+import { HttpStatusCode } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+type FeedType = 'videos' | 'video-comments' | 'subscriptions'
+
+export class FeedCommand extends AbstractCommand {
+
+  getXML (options: OverrideCommandOptions & {
+    feed: FeedType
+    format?: string
+  }) {
+    const { feed, format } = options
+    const path = '/feeds/' + feed + '.xml'
+
+    return this.getRequestText({
+      ...options,
+
+      path,
+      query: format ? { format } : undefined,
+      accept: 'application/xml',
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getJSON (options: OverrideCommandOptions & {
+    feed: FeedType
+    query?: { [ id: string ]: any }
+  }) {
+    const { feed, query } = options
+    const path = '/feeds/' + feed + '.json'
+
+    return this.getRequestText({
+      ...options,
+
+      path,
+      query,
+      accept: 'application/json',
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
diff --git a/shared/extra-utils/feeds/feeds.ts b/shared/extra-utils/feeds/feeds.ts
deleted file mode 100644 (file)
index ce0a98c..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-import * as request from 'supertest'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-type FeedType = 'videos' | 'video-comments' | 'subscriptions'
-
-function getXMLfeed (url: string, feed: FeedType, format?: string) {
-  const path = '/feeds/' + feed + '.xml'
-
-  return request(url)
-          .get(path)
-          .query((format) ? { format: format } : {})
-          .set('Accept', 'application/xml')
-          .expect(HttpStatusCode.OK_200)
-          .expect('Content-Type', /xml/)
-}
-
-function getJSONfeed (url: string, feed: FeedType, query: any = {}, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/feeds/' + feed + '.json'
-
-  return request(url)
-          .get(path)
-          .query(query)
-          .set('Accept', 'application/json')
-          .expect(statusCodeExpected)
-          .expect('Content-Type', /json/)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getXMLfeed,
-  getJSONfeed
-}
diff --git a/shared/extra-utils/feeds/index.ts b/shared/extra-utils/feeds/index.ts
new file mode 100644 (file)
index 0000000..662a22b
--- /dev/null
@@ -0,0 +1 @@
+export * from './feeds-command'
index 87ee8abba085c808f35aab3d40cbe480db1d4e3b..4b3636d068d80a21dbcf90af368e89778beecb52 100644 (file)
@@ -1,51 +1,15 @@
-export * from './bulk/bulk'
-
-export * from './cli/cli'
-
-export * from './custom-pages/custom-pages'
-
-export * from './feeds/feeds'
-
-export * from './mock-servers/mock-instances-index'
-
-export * from './miscs/email'
-export * from './miscs/sql'
-export * from './miscs/miscs'
-export * from './miscs/stubs'
-
-export * from './moderation/abuses'
-export * from './plugins/mock-blocklist'
-
-export * from './requests/check-api-params'
-export * from './requests/requests'
-
-export * from './search/video-channels'
-export * from './search/video-playlists'
-export * from './search/videos'
-
-export * from './server/activitypub'
-export * from './server/clients'
-export * from './server/config'
-export * from './server/debug'
-export * from './server/follows'
-export * from './server/jobs'
-export * from './server/plugins'
-export * from './server/servers'
-
-export * from './users/accounts'
-export * from './users/blocklist'
-export * from './users/login'
-export * from './users/user-notifications'
-export * from './users/user-subscriptions'
-export * from './users/users'
-
-export * from './videos/live'
-export * from './videos/services'
-export * from './videos/video-blacklist'
-export * from './videos/video-captions'
-export * from './videos/video-change-ownership'
-export * from './videos/video-channels'
-export * from './videos/video-comments'
-export * from './videos/video-playlists'
-export * from './videos/video-streaming-playlists'
-export * from './videos/videos'
+export * from './bulk'
+export * from './cli'
+export * from './custom-pages'
+export * from './feeds'
+export * from './logs'
+export * from './miscs'
+export * from './mock-servers'
+export * from './moderation'
+export * from './overviews'
+export * from './requests'
+export * from './search'
+export * from './server'
+export * from './socket'
+export * from './users'
+export * from './videos'
diff --git a/shared/extra-utils/logs/index.ts b/shared/extra-utils/logs/index.ts
new file mode 100644 (file)
index 0000000..69452d7
--- /dev/null
@@ -0,0 +1 @@
+export * from './logs-command'
diff --git a/shared/extra-utils/logs/logs-command.ts b/shared/extra-utils/logs/logs-command.ts
new file mode 100644 (file)
index 0000000..5912e81
--- /dev/null
@@ -0,0 +1,43 @@
+import { HttpStatusCode } from '@shared/models'
+import { LogLevel } from '../../models/server/log-level.type'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class LogsCommand extends AbstractCommand {
+
+  getLogs (options: OverrideCommandOptions & {
+    startDate: Date
+    endDate?: Date
+    level?: LogLevel
+  }) {
+    const { startDate, endDate, level } = options
+    const path = '/api/v1/server/logs'
+
+    return this.getRequestBody({
+      ...options,
+
+      path,
+      query: { startDate, endDate, level },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getAuditLogs (options: OverrideCommandOptions & {
+    startDate: Date
+    endDate?: Date
+  }) {
+    const { startDate, endDate } = options
+
+    const path = '/api/v1/server/audit-logs'
+
+    return this.getRequestBody({
+      ...options,
+
+      path,
+      query: { startDate, endDate },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+}
diff --git a/shared/extra-utils/logs/logs.ts b/shared/extra-utils/logs/logs.ts
deleted file mode 100644 (file)
index 8d74127..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-import { makeGetRequest } from '../requests/requests'
-import { LogLevel } from '../../models/server/log-level.type'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getLogs (url: string, accessToken: string, startDate: Date, endDate?: Date, level?: LogLevel) {
-  const path = '/api/v1/server/logs'
-
-  return makeGetRequest({
-    url,
-    path,
-    token: accessToken,
-    query: { startDate, endDate, level },
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getAuditLogs (url: string, accessToken: string, startDate: Date, endDate?: Date) {
-  const path = '/api/v1/server/audit-logs'
-
-  return makeGetRequest({
-    url,
-    path,
-    token: accessToken,
-    query: { startDate, endDate },
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-export {
-  getLogs,
-  getAuditLogs
-}
diff --git a/shared/extra-utils/miscs/checks.ts b/shared/extra-utils/miscs/checks.ts
new file mode 100644 (file)
index 0000000..7fc92f8
--- /dev/null
@@ -0,0 +1,46 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
+
+import { expect } from 'chai'
+import { pathExists, readFile } from 'fs-extra'
+import { join } from 'path'
+import { root } from '@server/helpers/core-utils'
+import { HttpStatusCode } from '@shared/models'
+import { makeGetRequest } from '../requests'
+import { PeerTubeServer } from '../server'
+
+// Default interval -> 5 minutes
+function dateIsValid (dateString: string, interval = 300000) {
+  const dateToCheck = new Date(dateString)
+  const now = new Date()
+
+  return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
+}
+
+async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
+  const res = await makeGetRequest({
+    url,
+    path: imagePath,
+    expectedStatus: HttpStatusCode.OK_200
+  })
+
+  const body = res.body
+
+  const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
+  const minLength = body.length - ((30 * body.length) / 100)
+  const maxLength = body.length + ((30 * body.length) / 100)
+
+  expect(data.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture')
+  expect(data.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture')
+}
+
+async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
+  const base = server.servers.buildDirectory(directory)
+
+  expect(await pathExists(join(base, filePath))).to.equal(exist)
+}
+
+export {
+  dateIsValid,
+  testImage,
+  testFileExistsOrNot
+}
diff --git a/shared/extra-utils/miscs/generate.ts b/shared/extra-utils/miscs/generate.ts
new file mode 100644 (file)
index 0000000..8d64354
--- /dev/null
@@ -0,0 +1,61 @@
+import * as ffmpeg from 'fluent-ffmpeg'
+import { ensureDir, pathExists } from 'fs-extra'
+import { dirname } from 'path'
+import { buildAbsoluteFixturePath } from './tests'
+
+async function generateHighBitrateVideo () {
+  const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
+
+  await ensureDir(dirname(tempFixturePath))
+
+  const exists = await pathExists(tempFixturePath)
+  if (!exists) {
+    console.log('Generating high bitrate video.')
+
+    // Generate a random, high bitrate video on the fly, so we don't have to include
+    // a large file in the repo. The video needs to have a certain minimum length so
+    // that FFmpeg properly applies bitrate limits.
+    // https://stackoverflow.com/a/15795112
+    return new Promise<string>((res, rej) => {
+      ffmpeg()
+        .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
+        .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
+        .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
+        .output(tempFixturePath)
+        .on('error', rej)
+        .on('end', () => res(tempFixturePath))
+        .run()
+    })
+  }
+
+  return tempFixturePath
+}
+
+async function generateVideoWithFramerate (fps = 60) {
+  const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
+
+  await ensureDir(dirname(tempFixturePath))
+
+  const exists = await pathExists(tempFixturePath)
+  if (!exists) {
+    console.log('Generating video with framerate %d.', fps)
+
+    return new Promise<string>((res, rej) => {
+      ffmpeg()
+        .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
+        .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
+        .outputOptions([ `-r ${fps}` ])
+        .output(tempFixturePath)
+        .on('error', rej)
+        .on('end', () => res(tempFixturePath))
+        .run()
+    })
+  }
+
+  return tempFixturePath
+}
+
+export {
+  generateHighBitrateVideo,
+  generateVideoWithFramerate
+}
diff --git a/shared/extra-utils/miscs/index.ts b/shared/extra-utils/miscs/index.ts
new file mode 100644 (file)
index 0000000..4474661
--- /dev/null
@@ -0,0 +1,5 @@
+export * from './checks'
+export * from './generate'
+export * from './sql-command'
+export * from './tests'
+export * from './webtorrent'
diff --git a/shared/extra-utils/miscs/miscs.ts b/shared/extra-utils/miscs/miscs.ts
deleted file mode 100644 (file)
index 462b914..0000000
+++ /dev/null
@@ -1,170 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-
-import * as chai from 'chai'
-import * as ffmpeg from 'fluent-ffmpeg'
-import { ensureDir, pathExists, readFile, stat } from 'fs-extra'
-import { basename, dirname, isAbsolute, join, resolve } from 'path'
-import * as request from 'supertest'
-import * as WebTorrent from 'webtorrent'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-const expect = chai.expect
-let webtorrent: WebTorrent.Instance
-
-function immutableAssign<T, U> (target: T, source: U) {
-  return Object.assign<{}, T, U>({}, target, source)
-}
-
-// Default interval -> 5 minutes
-function dateIsValid (dateString: string, interval = 300000) {
-  const dateToCheck = new Date(dateString)
-  const now = new Date()
-
-  return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
-}
-
-function wait (milliseconds: number) {
-  return new Promise(resolve => setTimeout(resolve, milliseconds))
-}
-
-function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
-  const WebTorrent = require('webtorrent')
-
-  if (!webtorrent) webtorrent = new WebTorrent()
-  if (refreshWebTorrent === true) webtorrent = new WebTorrent()
-
-  return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
-}
-
-function root () {
-  // We are in /miscs
-  let root = join(__dirname, '..', '..', '..')
-
-  if (basename(root) === 'dist') root = resolve(root, '..')
-
-  return root
-}
-
-function buildServerDirectory (server: { internalServerNumber: number }, directory: string) {
-  return join(root(), 'test' + server.internalServerNumber, directory)
-}
-
-async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
-  const res = await request(url)
-    .get(imagePath)
-    .expect(HttpStatusCode.OK_200)
-
-  const body = res.body
-
-  const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
-  const minLength = body.length - ((30 * body.length) / 100)
-  const maxLength = body.length + ((30 * body.length) / 100)
-
-  expect(data.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture')
-  expect(data.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture')
-}
-
-async function testFileExistsOrNot (server: { internalServerNumber: number }, directory: string, filePath: string, exist: boolean) {
-  const base = buildServerDirectory(server, directory)
-
-  expect(await pathExists(join(base, filePath))).to.equal(exist)
-}
-
-function isGithubCI () {
-  return !!process.env.GITHUB_WORKSPACE
-}
-
-function buildAbsoluteFixturePath (path: string, customCIPath = false) {
-  if (isAbsolute(path)) return path
-
-  if (customCIPath && process.env.GITHUB_WORKSPACE) {
-    return join(process.env.GITHUB_WORKSPACE, 'fixtures', path)
-  }
-
-  return join(root(), 'server', 'tests', 'fixtures', path)
-}
-
-function areHttpImportTestsDisabled () {
-  const disabled = process.env.DISABLE_HTTP_IMPORT_TESTS === 'true'
-
-  if (disabled) console.log('Import tests are disabled')
-
-  return disabled
-}
-
-async function generateHighBitrateVideo () {
-  const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
-
-  await ensureDir(dirname(tempFixturePath))
-
-  const exists = await pathExists(tempFixturePath)
-  if (!exists) {
-    console.log('Generating high bitrate video.')
-
-    // Generate a random, high bitrate video on the fly, so we don't have to include
-    // a large file in the repo. The video needs to have a certain minimum length so
-    // that FFmpeg properly applies bitrate limits.
-    // https://stackoverflow.com/a/15795112
-    return new Promise<string>((res, rej) => {
-      ffmpeg()
-        .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
-        .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
-        .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
-        .output(tempFixturePath)
-        .on('error', rej)
-        .on('end', () => res(tempFixturePath))
-        .run()
-    })
-  }
-
-  return tempFixturePath
-}
-
-async function generateVideoWithFramerate (fps = 60) {
-  const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
-
-  await ensureDir(dirname(tempFixturePath))
-
-  const exists = await pathExists(tempFixturePath)
-  if (!exists) {
-    console.log('Generating video with framerate %d.', fps)
-
-    return new Promise<string>((res, rej) => {
-      ffmpeg()
-        .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
-        .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
-        .outputOptions([ `-r ${fps}` ])
-        .output(tempFixturePath)
-        .on('error', rej)
-        .on('end', () => res(tempFixturePath))
-        .run()
-    })
-  }
-
-  return tempFixturePath
-}
-
-async function getFileSize (path: string) {
-  const stats = await stat(path)
-
-  return stats.size
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  dateIsValid,
-  wait,
-  areHttpImportTestsDisabled,
-  buildServerDirectory,
-  webtorrentAdd,
-  getFileSize,
-  immutableAssign,
-  testImage,
-  isGithubCI,
-  buildAbsoluteFixturePath,
-  testFileExistsOrNot,
-  root,
-  generateHighBitrateVideo,
-  generateVideoWithFramerate
-}
diff --git a/shared/extra-utils/miscs/sql-command.ts b/shared/extra-utils/miscs/sql-command.ts
new file mode 100644 (file)
index 0000000..80c8cd2
--- /dev/null
@@ -0,0 +1,142 @@
+import { QueryTypes, Sequelize } from 'sequelize'
+import { AbstractCommand } from '../shared/abstract-command'
+
+export class SQLCommand extends AbstractCommand {
+  private sequelize: Sequelize
+
+  deleteAll (table: string) {
+    const seq = this.getSequelize()
+
+    const options = { type: QueryTypes.DELETE }
+
+    return seq.query(`DELETE FROM "${table}"`, options)
+  }
+
+  async getCount (table: string) {
+    const seq = this.getSequelize()
+
+    const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
+
+    const [ { total } ] = await seq.query<{ total: string }>(`SELECT COUNT(*) as total FROM "${table}"`, options)
+    if (total === null) return 0
+
+    return parseInt(total, 10)
+  }
+
+  setActorField (to: string, field: string, value: string) {
+    const seq = this.getSequelize()
+
+    const options = { type: QueryTypes.UPDATE }
+
+    return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
+  }
+
+  setVideoField (uuid: string, field: string, value: string) {
+    const seq = this.getSequelize()
+
+    const options = { type: QueryTypes.UPDATE }
+
+    return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
+  }
+
+  setPlaylistField (uuid: string, field: string, value: string) {
+    const seq = this.getSequelize()
+
+    const options = { type: QueryTypes.UPDATE }
+
+    return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
+  }
+
+  async countVideoViewsOf (uuid: string) {
+    const seq = this.getSequelize()
+
+    // tslint:disable
+    const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
+      `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'`
+
+    const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
+    const [ { total } ] = await seq.query<{ total: number }>(query, options)
+
+    if (!total) return 0
+
+    return parseInt(total + '', 10)
+  }
+
+  getActorImage (filename: string) {
+    return this.selectQuery(`SELECT * FROM "actorImage" WHERE filename = '${filename}'`)
+      .then(rows => rows[0])
+  }
+
+  selectQuery (query: string) {
+    const seq = this.getSequelize()
+    const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
+
+    return seq.query<any>(query, options)
+  }
+
+  updateQuery (query: string) {
+    const seq = this.getSequelize()
+    const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE }
+
+    return seq.query(query, options)
+  }
+
+  setPluginField (pluginName: string, field: string, value: string) {
+    const seq = this.getSequelize()
+
+    const options = { type: QueryTypes.UPDATE }
+
+    return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
+  }
+
+  setPluginVersion (pluginName: string, newVersion: string) {
+    return this.setPluginField(pluginName, 'version', newVersion)
+  }
+
+  setPluginLatestVersion (pluginName: string, newVersion: string) {
+    return this.setPluginField(pluginName, 'latestVersion', newVersion)
+  }
+
+  setActorFollowScores (newScore: number) {
+    const seq = this.getSequelize()
+
+    const options = { type: QueryTypes.UPDATE }
+
+    return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options)
+  }
+
+  setTokenField (accessToken: string, field: string, value: string) {
+    const seq = this.getSequelize()
+
+    const options = { type: QueryTypes.UPDATE }
+
+    return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options)
+  }
+
+  async cleanup () {
+    if (!this.sequelize) return
+
+    await this.sequelize.close()
+    this.sequelize = undefined
+  }
+
+  private getSequelize () {
+    if (this.sequelize) return this.sequelize
+
+    const dbname = 'peertube_test' + this.server.internalServerNumber
+    const username = 'peertube'
+    const password = 'peertube'
+    const host = 'localhost'
+    const port = 5432
+
+    this.sequelize = new Sequelize(dbname, username, password, {
+      dialect: 'postgres',
+      host,
+      port,
+      logging: false
+    })
+
+    return this.sequelize
+  }
+
+}
diff --git a/shared/extra-utils/miscs/sql.ts b/shared/extra-utils/miscs/sql.ts
deleted file mode 100644 (file)
index 65a0aa5..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-import { QueryTypes, Sequelize } from 'sequelize'
-import { ServerInfo } from '../server/servers'
-
-const sequelizes: { [ id: number ]: Sequelize } = {}
-
-function getSequelize (internalServerNumber: number) {
-  if (sequelizes[internalServerNumber]) return sequelizes[internalServerNumber]
-
-  const dbname = 'peertube_test' + internalServerNumber
-  const username = 'peertube'
-  const password = 'peertube'
-  const host = 'localhost'
-  const port = 5432
-
-  const seq = new Sequelize(dbname, username, password, {
-    dialect: 'postgres',
-    host,
-    port,
-    logging: false
-  })
-
-  sequelizes[internalServerNumber] = seq
-
-  return seq
-}
-
-function deleteAll (internalServerNumber: number, table: string) {
-  const seq = getSequelize(internalServerNumber)
-
-  const options = { type: QueryTypes.DELETE }
-
-  return seq.query(`DELETE FROM "${table}"`, options)
-}
-
-async function getCount (internalServerNumber: number, table: string) {
-  const seq = getSequelize(internalServerNumber)
-
-  const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
-
-  const [ { total } ] = await seq.query<{ total: string }>(`SELECT COUNT(*) as total FROM "${table}"`, options)
-  if (total === null) return 0
-
-  return parseInt(total, 10)
-}
-
-function setActorField (internalServerNumber: number, to: string, field: string, value: string) {
-  const seq = getSequelize(internalServerNumber)
-
-  const options = { type: QueryTypes.UPDATE }
-
-  return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
-}
-
-function setVideoField (internalServerNumber: number, uuid: string, field: string, value: string) {
-  const seq = getSequelize(internalServerNumber)
-
-  const options = { type: QueryTypes.UPDATE }
-
-  return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
-}
-
-function setPlaylistField (internalServerNumber: number, uuid: string, field: string, value: string) {
-  const seq = getSequelize(internalServerNumber)
-
-  const options = { type: QueryTypes.UPDATE }
-
-  return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
-}
-
-async function countVideoViewsOf (internalServerNumber: number, uuid: string) {
-  const seq = getSequelize(internalServerNumber)
-
-  // tslint:disable
-  const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
-    `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'`
-
-  const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
-  const [ { total } ] = await seq.query<{ total: number }>(query, options)
-
-  if (!total) return 0
-
-  return parseInt(total + '', 10)
-}
-
-function getActorImage (internalServerNumber: number, filename: string) {
-  return selectQuery(internalServerNumber, `SELECT * FROM "actorImage" WHERE filename = '${filename}'`)
-    .then(rows => rows[0])
-}
-
-function selectQuery (internalServerNumber: number, query: string) {
-  const seq = getSequelize(internalServerNumber)
-  const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
-
-  return seq.query<any>(query, options)
-}
-
-function updateQuery (internalServerNumber: number, query: string) {
-  const seq = getSequelize(internalServerNumber)
-  const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE }
-
-  return seq.query(query, options)
-}
-
-async function closeAllSequelize (servers: ServerInfo[]) {
-  for (const server of servers) {
-    if (sequelizes[server.internalServerNumber]) {
-      await sequelizes[server.internalServerNumber].close()
-      // eslint-disable-next-line
-      delete sequelizes[server.internalServerNumber]
-    }
-  }
-}
-
-function setPluginField (internalServerNumber: number, pluginName: string, field: string, value: string) {
-  const seq = getSequelize(internalServerNumber)
-
-  const options = { type: QueryTypes.UPDATE }
-
-  return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
-}
-
-function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
-  return setPluginField(internalServerNumber, pluginName, 'version', newVersion)
-}
-
-function setPluginLatestVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
-  return setPluginField(internalServerNumber, pluginName, 'latestVersion', newVersion)
-}
-
-function setActorFollowScores (internalServerNumber: number, newScore: number) {
-  const seq = getSequelize(internalServerNumber)
-
-  const options = { type: QueryTypes.UPDATE }
-
-  return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options)
-}
-
-function setTokenField (internalServerNumber: number, accessToken: string, field: string, value: string) {
-  const seq = getSequelize(internalServerNumber)
-
-  const options = { type: QueryTypes.UPDATE }
-
-  return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options)
-}
-
-export {
-  setVideoField,
-  setPlaylistField,
-  setActorField,
-  countVideoViewsOf,
-  setPluginVersion,
-  setPluginLatestVersion,
-  selectQuery,
-  getActorImage,
-  deleteAll,
-  setTokenField,
-  updateQuery,
-  setActorFollowScores,
-  closeAllSequelize,
-  getCount
-}
diff --git a/shared/extra-utils/miscs/stubs.ts b/shared/extra-utils/miscs/stubs.ts
deleted file mode 100644 (file)
index d1eb0e3..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-function buildRequestStub (): any {
-  return { }
-}
-
-function buildResponseStub (): any {
-  return {
-    locals: {}
-  }
-}
-
-export {
-  buildResponseStub,
-  buildRequestStub
-}
diff --git a/shared/extra-utils/miscs/tests.ts b/shared/extra-utils/miscs/tests.ts
new file mode 100644 (file)
index 0000000..3dfb248
--- /dev/null
@@ -0,0 +1,94 @@
+import { stat } from 'fs-extra'
+import { basename, isAbsolute, join, resolve } from 'path'
+
+const FIXTURE_URLS = {
+  youtube: 'https://www.youtube.com/watch?v=msX3jv1XdvM',
+
+  /**
+   * The video is used to check format-selection correctness wrt. HDR,
+   * which brings its own set of oddities outside of a MediaSource.
+   * FIXME: refactor once HDR is supported at playback
+   *
+   * The video needs to have the following format_ids:
+   * (which you can check by using `youtube-dl <url> -F`):
+   * - 303 (1080p webm vp9)
+   * - 299 (1080p mp4 avc1)
+   * - 335 (1080p webm vp9.2 HDR)
+   *
+   * 15 jan. 2021: TEST VIDEO NOT CURRENTLY PROVIDING
+   * - 400 (1080p mp4 av01)
+   * - 315 (2160p webm vp9 HDR)
+   * - 337 (2160p webm vp9.2 HDR)
+   * - 401 (2160p mp4 av01 HDR)
+   */
+  youtubeHDR: 'https://www.youtube.com/watch?v=qR5vOXbZsI4',
+
+  // eslint-disable-next-line max-len
+  magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4',
+
+  badVideo: 'https://download.cpy.re/peertube/bad_video.mp4',
+  goodVideo: 'https://download.cpy.re/peertube/good_video.mp4',
+  video4K: 'https://download.cpy.re/peertube/4k_file.txt'
+}
+
+function parallelTests () {
+  return process.env.MOCHA_PARALLEL === 'true'
+}
+
+function isGithubCI () {
+  return !!process.env.GITHUB_WORKSPACE
+}
+
+function areHttpImportTestsDisabled () {
+  const disabled = process.env.DISABLE_HTTP_IMPORT_TESTS === 'true'
+
+  if (disabled) console.log('Import tests are disabled')
+
+  return disabled
+}
+
+function buildAbsoluteFixturePath (path: string, customCIPath = false) {
+  if (isAbsolute(path)) return path
+
+  if (customCIPath && process.env.GITHUB_WORKSPACE) {
+    return join(process.env.GITHUB_WORKSPACE, 'fixtures', path)
+  }
+
+  return join(root(), 'server', 'tests', 'fixtures', path)
+}
+
+function root () {
+  // We are in /miscs
+  let root = join(__dirname, '..', '..', '..')
+
+  if (basename(root) === 'dist') root = resolve(root, '..')
+
+  return root
+}
+
+function wait (milliseconds: number) {
+  return new Promise(resolve => setTimeout(resolve, milliseconds))
+}
+
+async function getFileSize (path: string) {
+  const stats = await stat(path)
+
+  return stats.size
+}
+
+function buildRequestStub (): any {
+  return { }
+}
+
+export {
+  FIXTURE_URLS,
+
+  parallelTests,
+  isGithubCI,
+  areHttpImportTestsDisabled,
+  buildAbsoluteFixturePath,
+  getFileSize,
+  buildRequestStub,
+  wait,
+  root
+}
diff --git a/shared/extra-utils/miscs/webtorrent.ts b/shared/extra-utils/miscs/webtorrent.ts
new file mode 100644 (file)
index 0000000..a1097ef
--- /dev/null
@@ -0,0 +1,31 @@
+import { readFile } from 'fs-extra'
+import * as parseTorrent from 'parse-torrent'
+import { basename, join } from 'path'
+import * as WebTorrent from 'webtorrent'
+import { VideoFile } from '@shared/models'
+import { PeerTubeServer } from '../server'
+
+let webtorrent: WebTorrent.Instance
+
+function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
+  const WebTorrent = require('webtorrent')
+
+  if (!webtorrent) webtorrent = new WebTorrent()
+  if (refreshWebTorrent === true) webtorrent = new WebTorrent()
+
+  return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
+}
+
+async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) {
+  const torrentName = basename(file.torrentUrl)
+  const torrentPath = server.servers.buildDirectory(join('torrents', torrentName))
+
+  const data = await readFile(torrentPath)
+
+  return parseTorrent(data)
+}
+
+export {
+  webtorrentAdd,
+  parseTorrentVideo
+}
diff --git a/shared/extra-utils/mock-servers/index.ts b/shared/extra-utils/mock-servers/index.ts
new file mode 100644 (file)
index 0000000..0ec07f6
--- /dev/null
@@ -0,0 +1,4 @@
+export * from './mock-email'
+export * from './mock-instances-index'
+export * from './mock-joinpeertube-versions'
+export * from './mock-plugin-blocklist'
similarity index 92%
rename from shared/extra-utils/miscs/email.ts
rename to shared/extra-utils/mock-servers/mock-email.ts
index 9fc9a5ad0312e670a8cd9b5fd7c2f27846e1075a..ffd62e3253a35c2093fc742d50ce05887332d404 100644 (file)
@@ -1,6 +1,6 @@
 import { ChildProcess } from 'child_process'
-import { randomInt } from '../../core-utils/miscs/miscs'
-import { parallelTests } from '../server/servers'
+import { randomInt } from '@shared/core-utils'
+import { parallelTests } from '../miscs'
 
 const MailDev = require('maildev')
 
diff --git a/shared/extra-utils/moderation/abuses-command.ts b/shared/extra-utils/moderation/abuses-command.ts
new file mode 100644 (file)
index 0000000..0db32ba
--- /dev/null
@@ -0,0 +1,228 @@
+import { pick } from '@shared/core-utils'
+import {
+  AbuseFilter,
+  AbuseMessage,
+  AbusePredefinedReasonsString,
+  AbuseState,
+  AbuseUpdate,
+  AbuseVideoIs,
+  AdminAbuse,
+  HttpStatusCode,
+  ResultList,
+  UserAbuse
+} from '@shared/models'
+import { unwrapBody } from '../requests/requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class AbusesCommand extends AbstractCommand {
+
+  report (options: OverrideCommandOptions & {
+    reason: string
+
+    accountId?: number
+    videoId?: number
+    commentId?: number
+
+    predefinedReasons?: AbusePredefinedReasonsString[]
+
+    startAt?: number
+    endAt?: number
+  }) {
+    const path = '/api/v1/abuses'
+
+    const video = options.videoId
+      ? {
+        id: options.videoId,
+        startAt: options.startAt,
+        endAt: options.endAt
+      }
+      : undefined
+
+    const comment = options.commentId
+      ? { id: options.commentId }
+      : undefined
+
+    const account = options.accountId
+      ? { id: options.accountId }
+      : undefined
+
+    const body = {
+      account,
+      video,
+      comment,
+
+      reason: options.reason,
+      predefinedReasons: options.predefinedReasons
+    }
+
+    return unwrapBody<{ abuse: { id: number } }>(this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: body,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  getAdminList (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+
+    id?: number
+    predefinedReason?: AbusePredefinedReasonsString
+    search?: string
+    filter?: AbuseFilter
+    state?: AbuseState
+    videoIs?: AbuseVideoIs
+    searchReporter?: string
+    searchReportee?: string
+    searchVideo?: string
+    searchVideoChannel?: string
+  } = {}) {
+    const toPick: (keyof typeof options)[] = [
+      'count',
+      'filter',
+      'id',
+      'predefinedReason',
+      'search',
+      'searchReportee',
+      'searchReporter',
+      'searchVideo',
+      'searchVideoChannel',
+      'sort',
+      'start',
+      'state',
+      'videoIs'
+    ]
+
+    const path = '/api/v1/abuses'
+
+    const defaultQuery = { sort: 'createdAt' }
+    const query = { ...defaultQuery, ...pick(options, toPick) }
+
+    return this.getRequestBody<ResultList<AdminAbuse>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getUserList (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+
+    id?: number
+    search?: string
+    state?: AbuseState
+  }) {
+    const toPick: (keyof typeof options)[] = [
+      'id',
+      'search',
+      'state',
+      'start',
+      'count',
+      'sort'
+    ]
+
+    const path = '/api/v1/users/me/abuses'
+
+    const defaultQuery = { sort: 'createdAt' }
+    const query = { ...defaultQuery, ...pick(options, toPick) }
+
+    return this.getRequestBody<ResultList<UserAbuse>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  update (options: OverrideCommandOptions & {
+    abuseId: number
+    body: AbuseUpdate
+  }) {
+    const { abuseId, body } = options
+    const path = '/api/v1/abuses/' + abuseId
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: body,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  delete (options: OverrideCommandOptions & {
+    abuseId: number
+  }) {
+    const { abuseId } = options
+    const path = '/api/v1/abuses/' + abuseId
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  listMessages (options: OverrideCommandOptions & {
+    abuseId: number
+  }) {
+    const { abuseId } = options
+    const path = '/api/v1/abuses/' + abuseId + '/messages'
+
+    return this.getRequestBody<ResultList<AbuseMessage>>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  deleteMessage (options: OverrideCommandOptions & {
+    abuseId: number
+    messageId: number
+  }) {
+    const { abuseId, messageId } = options
+    const path = '/api/v1/abuses/' + abuseId + '/messages/' + messageId
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  addMessage (options: OverrideCommandOptions & {
+    abuseId: number
+    message: string
+  }) {
+    const { abuseId, message } = options
+    const path = '/api/v1/abuses/' + abuseId + '/messages'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { message },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+}
diff --git a/shared/extra-utils/moderation/abuses.ts b/shared/extra-utils/moderation/abuses.ts
deleted file mode 100644 (file)
index c0fda72..0000000
+++ /dev/null
@@ -1,244 +0,0 @@
-import { AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
-import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function reportAbuse (options: {
-  url: string
-  token: string
-
-  reason: string
-
-  accountId?: number
-  videoId?: number
-  commentId?: number
-
-  predefinedReasons?: AbusePredefinedReasonsString[]
-
-  startAt?: number
-  endAt?: number
-
-  statusCodeExpected?: number
-}) {
-  const path = '/api/v1/abuses'
-
-  const video = options.videoId
-    ? {
-      id: options.videoId,
-      startAt: options.startAt,
-      endAt: options.endAt
-    }
-    : undefined
-
-  const comment = options.commentId
-    ? { id: options.commentId }
-    : undefined
-
-  const account = options.accountId
-    ? { id: options.accountId }
-    : undefined
-
-  const body = {
-    account,
-    video,
-    comment,
-
-    reason: options.reason,
-    predefinedReasons: options.predefinedReasons
-  }
-
-  return makePostBodyRequest({
-    url: options.url,
-    path,
-    token: options.token,
-
-    fields: body,
-    statusCodeExpected: options.statusCodeExpected || HttpStatusCode.OK_200
-  })
-}
-
-function getAdminAbusesList (options: {
-  url: string
-  token: string
-
-  start?: number
-  count?: number
-  sort?: string
-
-  id?: number
-  predefinedReason?: AbusePredefinedReasonsString
-  search?: string
-  filter?: AbuseFilter
-  state?: AbuseState
-  videoIs?: AbuseVideoIs
-  searchReporter?: string
-  searchReportee?: string
-  searchVideo?: string
-  searchVideoChannel?: string
-}) {
-  const {
-    url,
-    token,
-    start,
-    count,
-    sort,
-    id,
-    predefinedReason,
-    search,
-    filter,
-    state,
-    videoIs,
-    searchReporter,
-    searchReportee,
-    searchVideo,
-    searchVideoChannel
-  } = options
-  const path = '/api/v1/abuses'
-
-  const query = {
-    id,
-    predefinedReason,
-    search,
-    state,
-    filter,
-    videoIs,
-    start,
-    count,
-    sort: sort || 'createdAt',
-    searchReporter,
-    searchReportee,
-    searchVideo,
-    searchVideoChannel
-  }
-
-  return makeGetRequest({
-    url,
-    path,
-    token,
-    query,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getUserAbusesList (options: {
-  url: string
-  token: string
-
-  start?: number
-  count?: number
-  sort?: string
-
-  id?: number
-  search?: string
-  state?: AbuseState
-}) {
-  const {
-    url,
-    token,
-    start,
-    count,
-    sort,
-    id,
-    search,
-    state
-  } = options
-  const path = '/api/v1/users/me/abuses'
-
-  const query = {
-    id,
-    search,
-    state,
-    start,
-    count,
-    sort: sort || 'createdAt'
-  }
-
-  return makeGetRequest({
-    url,
-    path,
-    token,
-    query,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function updateAbuse (
-  url: string,
-  token: string,
-  abuseId: number,
-  body: AbuseUpdate,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/abuses/' + abuseId
-
-  return makePutBodyRequest({
-    url,
-    token,
-    path,
-    fields: body,
-    statusCodeExpected
-  })
-}
-
-function deleteAbuse (url: string, token: string, abuseId: number, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/abuses/' + abuseId
-
-  return makeDeleteRequest({
-    url,
-    token,
-    path,
-    statusCodeExpected
-  })
-}
-
-function listAbuseMessages (url: string, token: string, abuseId: number, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/abuses/' + abuseId + '/messages'
-
-  return makeGetRequest({
-    url,
-    token,
-    path,
-    statusCodeExpected
-  })
-}
-
-function deleteAbuseMessage (
-  url: string,
-  token: string,
-  abuseId: number,
-  messageId: number,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/abuses/' + abuseId + '/messages/' + messageId
-
-  return makeDeleteRequest({
-    url,
-    token,
-    path,
-    statusCodeExpected
-  })
-}
-
-function addAbuseMessage (url: string, token: string, abuseId: number, message: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/abuses/' + abuseId + '/messages'
-
-  return makePostBodyRequest({
-    url,
-    token,
-    path,
-    fields: { message },
-    statusCodeExpected
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  reportAbuse,
-  getAdminAbusesList,
-  updateAbuse,
-  deleteAbuse,
-  getUserAbusesList,
-  listAbuseMessages,
-  deleteAbuseMessage,
-  addAbuseMessage
-}
diff --git a/shared/extra-utils/moderation/index.ts b/shared/extra-utils/moderation/index.ts
new file mode 100644 (file)
index 0000000..b376439
--- /dev/null
@@ -0,0 +1 @@
+export * from './abuses-command'
diff --git a/shared/extra-utils/overviews/index.ts b/shared/extra-utils/overviews/index.ts
new file mode 100644 (file)
index 0000000..e195519
--- /dev/null
@@ -0,0 +1 @@
+export * from './overviews-command'
diff --git a/shared/extra-utils/overviews/overviews-command.ts b/shared/extra-utils/overviews/overviews-command.ts
new file mode 100644 (file)
index 0000000..06b4892
--- /dev/null
@@ -0,0 +1,23 @@
+import { HttpStatusCode, VideosOverview } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class OverviewsCommand extends AbstractCommand {
+
+  getVideos (options: OverrideCommandOptions & {
+    page: number
+  }) {
+    const { page } = options
+    const path = '/api/v1/overviews/videos'
+
+    const query = { page }
+
+    return this.getRequestBody<VideosOverview>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
diff --git a/shared/extra-utils/overviews/overviews.ts b/shared/extra-utils/overviews/overviews.ts
deleted file mode 100644 (file)
index 5e1a13e..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import { makeGetRequest } from '../requests/requests'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getVideosOverview (url: string, page: number, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/overviews/videos'
-
-  const query = { page }
-
-  return makeGetRequest({
-    url,
-    path,
-    query,
-    statusCodeExpected
-  })
-}
-
-function getVideosOverviewWithToken (url: string, page: number, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/overviews/videos'
-
-  const query = { page }
-
-  return makeGetRequest({
-    url,
-    path,
-    query,
-    token,
-    statusCodeExpected
-  })
-}
-
-export {
-  getVideosOverview,
-  getVideosOverviewWithToken
-}
index ecd8ce82389d5e31de01fbd78922992c07cb594c..4ae878384bba5543e59ca6bde62a42121b73db32 100644 (file)
@@ -1,7 +1,7 @@
+import { activityPubContextify } from '../../../server/helpers/activitypub'
 import { doRequest } from '../../../server/helpers/requests'
 import { HTTP_SIGNATURE } from '../../../server/initializers/constants'
 import { buildGlobalHeaders } from '../../../server/lib/job-queue/handlers/utils/activitypub-http-utils'
-import { activityPubContextify } from '../../../server/helpers/activitypub'
 
 function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
   const options = {
index 7f5ff775cfa05e11478cb9a4c0fe609c48d2f418..26ba1e9132eb6ec783d6e61ad5b59832e85ffd8b 100644 (file)
@@ -1,14 +1,13 @@
+import { HttpStatusCode } from '@shared/models'
 import { makeGetRequest } from './requests'
-import { immutableAssign } from '../miscs/miscs'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 
 function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
   return makeGetRequest({
     url,
     path,
     token,
-    query: immutableAssign(query, { start: 'hello' }),
-    statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+    query: { ...query, start: 'hello' },
+    expectedStatus: HttpStatusCode.BAD_REQUEST_400
   })
 }
 
@@ -17,16 +16,16 @@ async function checkBadCountPagination (url: string, path: string, token?: strin
     url,
     path,
     token,
-    query: immutableAssign(query, { count: 'hello' }),
-    statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+    query: { ...query, count: 'hello' },
+    expectedStatus: HttpStatusCode.BAD_REQUEST_400
   })
 
   await makeGetRequest({
     url,
     path,
     token,
-    query: immutableAssign(query, { count: 2000 }),
-    statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+    query: { ...query, count: 2000 },
+    expectedStatus: HttpStatusCode.BAD_REQUEST_400
   })
 }
 
@@ -35,8 +34,8 @@ function checkBadSortPagination (url: string, path: string, token?: string, quer
     url,
     path,
     token,
-    query: immutableAssign(query, { sort: 'hello' }),
-    statusCodeExpected: HttpStatusCode.BAD_REQUEST_400
+    query: { ...query, sort: 'hello' },
+    expectedStatus: HttpStatusCode.BAD_REQUEST_400
   })
 }
 
diff --git a/shared/extra-utils/requests/index.ts b/shared/extra-utils/requests/index.ts
new file mode 100644 (file)
index 0000000..501163f
--- /dev/null
@@ -0,0 +1,3 @@
+// Don't include activitypub that import stuff from server
+export * from './check-api-params'
+export * from './requests'
index 38e24d89716967b8c9fc04da1177a11d603885ad..70f7902222d2fb795fe1670e9c2a6068c61f8daa 100644 (file)
-/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
+/* eslint-disable @typescript-eslint/no-floating-promises */
 
+import { decode } from 'querystring'
 import * as request from 'supertest'
-import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
-import { isAbsolute, join } from 'path'
 import { URL } from 'url'
-import { decode } from 'querystring'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { HttpStatusCode } from '@shared/models'
+import { buildAbsoluteFixturePath } from '../miscs/tests'
 
-function get4KFileUrl () {
-  return 'https://download.cpy.re/peertube/4k_file.txt'
+export type CommonRequestParams = {
+  url: string
+  path?: string
+  contentType?: string
+  range?: string
+  redirects?: number
+  accept?: string
+  host?: string
+  token?: string
+  headers?: { [ name: string ]: string }
+  type?: string
+  xForwardedFor?: string
+  expectedStatus?: HttpStatusCode
 }
 
-function makeRawRequest (url: string, statusCodeExpected?: HttpStatusCode, range?: string) {
+function makeRawRequest (url: string, expectedStatus?: HttpStatusCode, range?: string) {
   const { host, protocol, pathname } = new URL(url)
 
-  return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected, range })
+  return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, expectedStatus, range })
 }
 
-function makeGetRequest (options: {
-  url: string
-  path?: string
+function makeGetRequest (options: CommonRequestParams & {
   query?: any
-  token?: string
-  statusCodeExpected?: HttpStatusCode
-  contentType?: string
-  range?: string
-  redirects?: number
-  accept?: string
 }) {
-  if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
-  if (options.contentType === undefined) options.contentType = 'application/json'
-
   const req = request(options.url).get(options.path)
+                                  .query(options.query)
 
-  if (options.contentType) req.set('Accept', options.contentType)
-  if (options.token) req.set('Authorization', 'Bearer ' + options.token)
-  if (options.query) req.query(options.query)
-  if (options.range) req.set('Range', options.range)
-  if (options.accept) req.set('Accept', options.accept)
-  if (options.redirects) req.redirects(options.redirects)
-
-  return req.expect(options.statusCodeExpected)
+  return buildRequest(req, { contentType: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
 }
 
-function makeDeleteRequest (options: {
-  url: string
-  path: string
-  token?: string
-  statusCodeExpected?: HttpStatusCode
-}) {
-  if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
+function makeHTMLRequest (url: string, path: string) {
+  return makeGetRequest({
+    url,
+    path,
+    accept: 'text/html',
+    expectedStatus: HttpStatusCode.OK_200
+  })
+}
 
-  const req = request(options.url)
-    .delete(options.path)
-    .set('Accept', 'application/json')
+function makeActivityPubGetRequest (url: string, path: string, expectedStatus = HttpStatusCode.OK_200) {
+  return makeGetRequest({
+    url,
+    path,
+    expectedStatus: expectedStatus,
+    accept: 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8'
+  })
+}
 
-  if (options.token) req.set('Authorization', 'Bearer ' + options.token)
+function makeDeleteRequest (options: CommonRequestParams) {
+  const req = request(options.url).delete(options.path)
 
-  return req.expect(options.statusCodeExpected)
+  return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
 }
 
-function makeUploadRequest (options: {
-  url: string
+function makeUploadRequest (options: CommonRequestParams & {
   method?: 'POST' | 'PUT'
-  path: string
-  token?: string
+
   fields: { [ fieldName: string ]: any }
   attaches?: { [ attachName: string ]: any | any[] }
-  statusCodeExpected?: HttpStatusCode
 }) {
-  if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
-
-  let req: request.Test
-  if (options.method === 'PUT') {
-    req = request(options.url).put(options.path)
-  } else {
-    req = request(options.url).post(options.path)
-  }
-
-  req.set('Accept', 'application/json')
+  let req = options.method === 'PUT'
+    ? request(options.url).put(options.path)
+    : request(options.url).post(options.path)
 
-  if (options.token) req.set('Authorization', 'Bearer ' + options.token)
+  req = buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
 
-  Object.keys(options.fields).forEach(field => {
-    const value = options.fields[field]
-
-    if (value === undefined) return
-
-    if (Array.isArray(value)) {
-      for (let i = 0; i < value.length; i++) {
-        req.field(field + '[' + i + ']', value[i])
-      }
-    } else {
-      req.field(field, value)
-    }
-  })
+  buildFields(req, options.fields)
 
   Object.keys(options.attaches || {}).forEach(attach => {
     const value = options.attaches[attach]
+
     if (Array.isArray(value)) {
       req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1])
     } else {
@@ -105,27 +84,16 @@ function makeUploadRequest (options: {
     }
   })
 
-  return req.expect(options.statusCodeExpected)
+  return req
 }
 
-function makePostBodyRequest (options: {
-  url: string
-  path: string
-  token?: string
+function makePostBodyRequest (options: CommonRequestParams & {
   fields?: { [ fieldName: string ]: any }
-  statusCodeExpected?: HttpStatusCode
 }) {
-  if (!options.fields) options.fields = {}
-  if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
-
-  const req = request(options.url)
-                .post(options.path)
-                .set('Accept', 'application/json')
+  const req = request(options.url).post(options.path)
+                                  .send(options.fields)
 
-  if (options.token) req.set('Authorization', 'Bearer ' + options.token)
-
-  return req.send(options.fields)
-            .expect(options.statusCodeExpected)
+  return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
 }
 
 function makePutBodyRequest (options: {
@@ -133,59 +101,29 @@ function makePutBodyRequest (options: {
   path: string
   token?: string
   fields: { [ fieldName: string ]: any }
-  statusCodeExpected?: HttpStatusCode
+  expectedStatus?: HttpStatusCode
 }) {
-  if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
-
-  const req = request(options.url)
-                .put(options.path)
-                .set('Accept', 'application/json')
+  const req = request(options.url).put(options.path)
+                                  .send(options.fields)
 
-  if (options.token) req.set('Authorization', 'Bearer ' + options.token)
-
-  return req.send(options.fields)
-            .expect(options.statusCodeExpected)
+  return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
 }
 
-function makeHTMLRequest (url: string, path: string) {
-  return request(url)
-    .get(path)
-    .set('Accept', 'text/html')
-    .expect(HttpStatusCode.OK_200)
+function decodeQueryString (path: string) {
+  return decode(path.split('?')[1])
 }
 
-function updateImageRequest (options: {
-  url: string
-  path: string
-  accessToken: string
-  fixture: string
-  fieldname: string
-}) {
-  let filePath = ''
-  if (isAbsolute(options.fixture)) {
-    filePath = options.fixture
-  } else {
-    filePath = join(root(), 'server', 'tests', 'fixtures', options.fixture)
-  }
-
-  return makeUploadRequest({
-    url: options.url,
-    path: options.path,
-    token: options.accessToken,
-    fields: {},
-    attaches: { [options.fieldname]: filePath },
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
+function unwrapBody <T> (test: request.Test): Promise<T> {
+  return test.then(res => res.body)
 }
 
-function decodeQueryString (path: string) {
-  return decode(path.split('?')[1])
+function unwrapText (test: request.Test): Promise<string> {
+  return test.then(res => res.text)
 }
 
 // ---------------------------------------------------------------------------
 
 export {
-  get4KFileUrl,
   makeHTMLRequest,
   makeGetRequest,
   decodeQueryString,
@@ -194,5 +132,51 @@ export {
   makePutBodyRequest,
   makeDeleteRequest,
   makeRawRequest,
-  updateImageRequest
+  makeActivityPubGetRequest,
+  unwrapBody,
+  unwrapText
+}
+
+// ---------------------------------------------------------------------------
+
+function buildRequest (req: request.Test, options: CommonRequestParams) {
+  if (options.contentType) req.set('Accept', options.contentType)
+  if (options.token) req.set('Authorization', 'Bearer ' + options.token)
+  if (options.range) req.set('Range', options.range)
+  if (options.accept) req.set('Accept', options.accept)
+  if (options.host) req.set('Host', options.host)
+  if (options.redirects) req.redirects(options.redirects)
+  if (options.expectedStatus) req.expect(options.expectedStatus)
+  if (options.xForwardedFor) req.set('X-Forwarded-For', options.xForwardedFor)
+  if (options.type) req.type(options.type)
+
+  Object.keys(options.headers || {}).forEach(name => {
+    req.set(name, options.headers[name])
+  })
+
+  return req
+}
+
+function buildFields (req: request.Test, fields: { [ fieldName: string ]: any }, namespace?: string) {
+  if (!fields) return
+
+  let formKey: string
+
+  for (const key of Object.keys(fields)) {
+    if (namespace) formKey = `${namespace}[${key}]`
+    else formKey = key
+
+    if (fields[key] === undefined) continue
+
+    if (Array.isArray(fields[key]) && fields[key].length === 0) {
+      req.field(key, null)
+      continue
+    }
+
+    if (fields[key] !== null && typeof fields[key] === 'object') {
+      buildFields(req, fields[key], formKey)
+    } else {
+      req.field(formKey, fields[key])
+    }
+  }
 }
diff --git a/shared/extra-utils/search/index.ts b/shared/extra-utils/search/index.ts
new file mode 100644 (file)
index 0000000..48dbe8a
--- /dev/null
@@ -0,0 +1 @@
+export * from './search-command'
diff --git a/shared/extra-utils/search/search-command.ts b/shared/extra-utils/search/search-command.ts
new file mode 100644 (file)
index 0000000..0fbbcd6
--- /dev/null
@@ -0,0 +1,98 @@
+import {
+  HttpStatusCode,
+  ResultList,
+  Video,
+  VideoChannel,
+  VideoChannelsSearchQuery,
+  VideoPlaylist,
+  VideoPlaylistsSearchQuery,
+  VideosSearchQuery
+} from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class SearchCommand extends AbstractCommand {
+
+  searchChannels (options: OverrideCommandOptions & {
+    search: string
+  }) {
+    return this.advancedChannelSearch({
+      ...options,
+
+      search: { search: options.search }
+    })
+  }
+
+  advancedChannelSearch (options: OverrideCommandOptions & {
+    search: VideoChannelsSearchQuery
+  }) {
+    const { search } = options
+    const path = '/api/v1/search/video-channels'
+
+    return this.getRequestBody<ResultList<VideoChannel>>({
+      ...options,
+
+      path,
+      query: search,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  searchPlaylists (options: OverrideCommandOptions & {
+    search: string
+  }) {
+    return this.advancedPlaylistSearch({
+      ...options,
+
+      search: { search: options.search }
+    })
+  }
+
+  advancedPlaylistSearch (options: OverrideCommandOptions & {
+    search: VideoPlaylistsSearchQuery
+  }) {
+    const { search } = options
+    const path = '/api/v1/search/video-playlists'
+
+    return this.getRequestBody<ResultList<VideoPlaylist>>({
+      ...options,
+
+      path,
+      query: search,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  searchVideos (options: OverrideCommandOptions & {
+    search: string
+    sort?: string
+  }) {
+    const { search, sort } = options
+
+    return this.advancedVideoSearch({
+      ...options,
+
+      search: {
+        search: search,
+        sort: sort ?? '-publishedAt'
+      }
+    })
+  }
+
+  advancedVideoSearch (options: OverrideCommandOptions & {
+    search: VideosSearchQuery
+  }) {
+    const { search } = options
+    const path = '/api/v1/search/videos'
+
+    return this.getRequestBody<ResultList<Video>>({
+      ...options,
+
+      path,
+      query: search,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
diff --git a/shared/extra-utils/search/video-channels.ts b/shared/extra-utils/search/video-channels.ts
deleted file mode 100644 (file)
index 8e0f425..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-import { VideoChannelsSearchQuery } from '@shared/models'
-import { makeGetRequest } from '../requests/requests'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function searchVideoChannel (url: string, search: string, token?: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/search/video-channels'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: {
-      sort: '-createdAt',
-      search
-    },
-    token,
-    statusCodeExpected
-  })
-}
-
-function advancedVideoChannelSearch (url: string, search: VideoChannelsSearchQuery) {
-  const path = '/api/v1/search/video-channels'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: search,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  searchVideoChannel,
-  advancedVideoChannelSearch
-}
diff --git a/shared/extra-utils/search/video-playlists.ts b/shared/extra-utils/search/video-playlists.ts
deleted file mode 100644 (file)
index c22831d..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-import { VideoPlaylistsSearchQuery } from '@shared/models'
-import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
-import { makeGetRequest } from '../requests/requests'
-
-function searchVideoPlaylists (url: string, search: string, token?: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/search/video-playlists'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: {
-      sort: '-createdAt',
-      search
-    },
-    token,
-    statusCodeExpected
-  })
-}
-
-function advancedVideoPlaylistSearch (url: string, search: VideoPlaylistsSearchQuery) {
-  const path = '/api/v1/search/video-playlists'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: search,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  searchVideoPlaylists,
-  advancedVideoPlaylistSearch
-}
diff --git a/shared/extra-utils/search/videos.ts b/shared/extra-utils/search/videos.ts
deleted file mode 100644 (file)
index db6edbd..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-
-import * as request from 'supertest'
-import { VideosSearchQuery } from '../../models/search'
-import { immutableAssign } from '../miscs/miscs'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function searchVideo (url: string, search: string, sort = '-publishedAt') {
-  const path = '/api/v1/search/videos'
-
-  const query = { sort, search: search }
-  const req = request(url)
-    .get(path)
-    .query(query)
-    .set('Accept', 'application/json')
-
-  return req.expect(HttpStatusCode.OK_200)
-            .expect('Content-Type', /json/)
-}
-
-function searchVideoWithToken (url: string, search: string, token: string, query: { nsfw?: boolean } = {}) {
-  const path = '/api/v1/search/videos'
-  const req = request(url)
-    .get(path)
-    .set('Authorization', 'Bearer ' + token)
-    .query(immutableAssign(query, { sort: '-publishedAt', search }))
-    .set('Accept', 'application/json')
-
-  return req.expect(HttpStatusCode.OK_200)
-            .expect('Content-Type', /json/)
-}
-
-function searchVideoWithSort (url: string, search: string, sort: string) {
-  const path = '/api/v1/search/videos'
-
-  const query = { search, sort }
-
-  return request(url)
-    .get(path)
-    .query(query)
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function advancedVideosSearch (url: string, options: VideosSearchQuery) {
-  const path = '/api/v1/search/videos'
-
-  return request(url)
-    .get(path)
-    .query(options)
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  searchVideo,
-  advancedVideosSearch,
-  searchVideoWithToken,
-  searchVideoWithSort
-}
diff --git a/shared/extra-utils/server/activitypub.ts b/shared/extra-utils/server/activitypub.ts
deleted file mode 100644 (file)
index cf967ed..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as request from 'supertest'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function makeActivityPubGetRequest (url: string, path: string, expectedStatus = HttpStatusCode.OK_200) {
-  return request(url)
-    .get(path)
-    .set('Accept', 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8')
-    .expect(expectedStatus)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  makeActivityPubGetRequest
-}
diff --git a/shared/extra-utils/server/clients.ts b/shared/extra-utils/server/clients.ts
deleted file mode 100644 (file)
index 894fe49..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as request from 'supertest'
-import { URL } from 'url'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getClient (url: string) {
-  const path = '/api/v1/oauth-clients/local'
-
-  return request(url)
-          .get(path)
-          .set('Host', new URL(url).host)
-          .set('Accept', 'application/json')
-          .expect(HttpStatusCode.OK_200)
-          .expect('Content-Type', /json/)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getClient
-}
diff --git a/shared/extra-utils/server/config-command.ts b/shared/extra-utils/server/config-command.ts
new file mode 100644 (file)
index 0000000..11148aa
--- /dev/null
@@ -0,0 +1,263 @@
+import { merge } from 'lodash'
+import { DeepPartial } from '@shared/core-utils'
+import { About, HttpStatusCode, ServerConfig } from '@shared/models'
+import { CustomConfig } from '../../models/server/custom-config.model'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class ConfigCommand extends AbstractCommand {
+
+  static getCustomConfigResolutions (enabled: boolean) {
+    return {
+      '240p': enabled,
+      '360p': enabled,
+      '480p': enabled,
+      '720p': enabled,
+      '1080p': enabled,
+      '1440p': enabled,
+      '2160p': enabled
+    }
+  }
+
+  getConfig (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/config'
+
+    return this.getRequestBody<ServerConfig>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getAbout (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/config/about'
+
+    return this.getRequestBody<About>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getCustomConfig (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/config/custom'
+
+    return this.getRequestBody<CustomConfig>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  updateCustomConfig (options: OverrideCommandOptions & {
+    newCustomConfig: CustomConfig
+  }) {
+    const path = '/api/v1/config/custom'
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: options.newCustomConfig,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  deleteCustomConfig (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/config/custom'
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  updateCustomSubConfig (options: OverrideCommandOptions & {
+    newConfig: DeepPartial<CustomConfig>
+  }) {
+    const newCustomConfig: CustomConfig = {
+      instance: {
+        name: 'PeerTube updated',
+        shortDescription: 'my short description',
+        description: 'my super description',
+        terms: 'my super terms',
+        codeOfConduct: 'my super coc',
+
+        creationReason: 'my super creation reason',
+        moderationInformation: 'my super moderation information',
+        administrator: 'Kuja',
+        maintenanceLifetime: 'forever',
+        businessModel: 'my super business model',
+        hardwareInformation: '2vCore 3GB RAM',
+
+        languages: [ 'en', 'es' ],
+        categories: [ 1, 2 ],
+
+        isNSFW: true,
+        defaultNSFWPolicy: 'blur',
+
+        defaultClientRoute: '/videos/recently-added',
+
+        customizations: {
+          javascript: 'alert("coucou")',
+          css: 'body { background-color: red; }'
+        }
+      },
+      theme: {
+        default: 'default'
+      },
+      services: {
+        twitter: {
+          username: '@MySuperUsername',
+          whitelisted: true
+        }
+      },
+      cache: {
+        previews: {
+          size: 2
+        },
+        captions: {
+          size: 3
+        },
+        torrents: {
+          size: 4
+        }
+      },
+      signup: {
+        enabled: false,
+        limit: 5,
+        requiresEmailVerification: false,
+        minimumAge: 16
+      },
+      admin: {
+        email: 'superadmin1@example.com'
+      },
+      contactForm: {
+        enabled: true
+      },
+      user: {
+        videoQuota: 5242881,
+        videoQuotaDaily: 318742
+      },
+      transcoding: {
+        enabled: true,
+        allowAdditionalExtensions: true,
+        allowAudioFiles: true,
+        threads: 1,
+        concurrency: 3,
+        profile: 'default',
+        resolutions: {
+          '0p': false,
+          '240p': false,
+          '360p': true,
+          '480p': true,
+          '720p': false,
+          '1080p': false,
+          '1440p': false,
+          '2160p': false
+        },
+        webtorrent: {
+          enabled: true
+        },
+        hls: {
+          enabled: false
+        }
+      },
+      live: {
+        enabled: true,
+        allowReplay: false,
+        maxDuration: -1,
+        maxInstanceLives: -1,
+        maxUserLives: 50,
+        transcoding: {
+          enabled: true,
+          threads: 4,
+          profile: 'default',
+          resolutions: {
+            '240p': true,
+            '360p': true,
+            '480p': true,
+            '720p': true,
+            '1080p': true,
+            '1440p': true,
+            '2160p': true
+          }
+        }
+      },
+      import: {
+        videos: {
+          concurrency: 3,
+          http: {
+            enabled: false
+          },
+          torrent: {
+            enabled: false
+          }
+        }
+      },
+      trending: {
+        videos: {
+          algorithms: {
+            enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
+            default: 'hot'
+          }
+        }
+      },
+      autoBlacklist: {
+        videos: {
+          ofUsers: {
+            enabled: false
+          }
+        }
+      },
+      followers: {
+        instance: {
+          enabled: true,
+          manualApproval: false
+        }
+      },
+      followings: {
+        instance: {
+          autoFollowBack: {
+            enabled: false
+          },
+          autoFollowIndex: {
+            indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
+            enabled: false
+          }
+        }
+      },
+      broadcastMessage: {
+        enabled: true,
+        level: 'warning',
+        message: 'hello',
+        dismissable: true
+      },
+      search: {
+        remoteUri: {
+          users: true,
+          anonymous: true
+        },
+        searchIndex: {
+          enabled: true,
+          url: 'https://search.joinpeertube.org',
+          disableLocalSearch: true,
+          isDefaultSearch: true
+        }
+      }
+    }
+
+    merge(newCustomConfig, options.newConfig)
+
+    return this.updateCustomConfig({ ...options, newCustomConfig })
+  }
+}
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
deleted file mode 100644 (file)
index 9fcfb31..0000000
+++ /dev/null
@@ -1,260 +0,0 @@
-import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
-import { CustomConfig } from '../../models/server/custom-config.model'
-import { DeepPartial, HttpStatusCode } from '@shared/core-utils'
-import { merge } from 'lodash'
-
-function getConfig (url: string) {
-  const path = '/api/v1/config'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getAbout (url: string) {
-  const path = '/api/v1/config/about'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/config/custom'
-
-  return makeGetRequest({
-    url,
-    token,
-    path,
-    statusCodeExpected
-  })
-}
-
-function updateCustomConfig (url: string, token: string, newCustomConfig: CustomConfig, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/config/custom'
-
-  return makePutBodyRequest({
-    url,
-    token,
-    path,
-    fields: newCustomConfig,
-    statusCodeExpected
-  })
-}
-
-function updateCustomSubConfig (url: string, token: string, newConfig: DeepPartial<CustomConfig>) {
-  const updateParams: CustomConfig = {
-    instance: {
-      name: 'PeerTube updated',
-      shortDescription: 'my short description',
-      description: 'my super description',
-      terms: 'my super terms',
-      codeOfConduct: 'my super coc',
-
-      creationReason: 'my super creation reason',
-      moderationInformation: 'my super moderation information',
-      administrator: 'Kuja',
-      maintenanceLifetime: 'forever',
-      businessModel: 'my super business model',
-      hardwareInformation: '2vCore 3GB RAM',
-
-      languages: [ 'en', 'es' ],
-      categories: [ 1, 2 ],
-
-      isNSFW: true,
-      defaultNSFWPolicy: 'blur',
-
-      defaultClientRoute: '/videos/recently-added',
-
-      customizations: {
-        javascript: 'alert("coucou")',
-        css: 'body { background-color: red; }'
-      }
-    },
-    theme: {
-      default: 'default'
-    },
-    services: {
-      twitter: {
-        username: '@MySuperUsername',
-        whitelisted: true
-      }
-    },
-    cache: {
-      previews: {
-        size: 2
-      },
-      captions: {
-        size: 3
-      },
-      torrents: {
-        size: 4
-      }
-    },
-    signup: {
-      enabled: false,
-      limit: 5,
-      requiresEmailVerification: false,
-      minimumAge: 16
-    },
-    admin: {
-      email: 'superadmin1@example.com'
-    },
-    contactForm: {
-      enabled: true
-    },
-    user: {
-      videoQuota: 5242881,
-      videoQuotaDaily: 318742
-    },
-    transcoding: {
-      enabled: true,
-      allowAdditionalExtensions: true,
-      allowAudioFiles: true,
-      threads: 1,
-      concurrency: 3,
-      profile: 'default',
-      resolutions: {
-        '0p': false,
-        '240p': false,
-        '360p': true,
-        '480p': true,
-        '720p': false,
-        '1080p': false,
-        '1440p': false,
-        '2160p': false
-      },
-      webtorrent: {
-        enabled: true
-      },
-      hls: {
-        enabled: false
-      }
-    },
-    live: {
-      enabled: true,
-      allowReplay: false,
-      maxDuration: -1,
-      maxInstanceLives: -1,
-      maxUserLives: 50,
-      transcoding: {
-        enabled: true,
-        threads: 4,
-        profile: 'default',
-        resolutions: {
-          '240p': true,
-          '360p': true,
-          '480p': true,
-          '720p': true,
-          '1080p': true,
-          '1440p': true,
-          '2160p': true
-        }
-      }
-    },
-    import: {
-      videos: {
-        concurrency: 3,
-        http: {
-          enabled: false
-        },
-        torrent: {
-          enabled: false
-        }
-      }
-    },
-    trending: {
-      videos: {
-        algorithms: {
-          enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
-          default: 'hot'
-        }
-      }
-    },
-    autoBlacklist: {
-      videos: {
-        ofUsers: {
-          enabled: false
-        }
-      }
-    },
-    followers: {
-      instance: {
-        enabled: true,
-        manualApproval: false
-      }
-    },
-    followings: {
-      instance: {
-        autoFollowBack: {
-          enabled: false
-        },
-        autoFollowIndex: {
-          indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
-          enabled: false
-        }
-      }
-    },
-    broadcastMessage: {
-      enabled: true,
-      level: 'warning',
-      message: 'hello',
-      dismissable: true
-    },
-    search: {
-      remoteUri: {
-        users: true,
-        anonymous: true
-      },
-      searchIndex: {
-        enabled: true,
-        url: 'https://search.joinpeertube.org',
-        disableLocalSearch: true,
-        isDefaultSearch: true
-      }
-    }
-  }
-
-  merge(updateParams, newConfig)
-
-  return updateCustomConfig(url, token, updateParams)
-}
-
-function getCustomConfigResolutions (enabled: boolean) {
-  return {
-    '240p': enabled,
-    '360p': enabled,
-    '480p': enabled,
-    '720p': enabled,
-    '1080p': enabled,
-    '1440p': enabled,
-    '2160p': enabled
-  }
-}
-
-function deleteCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/config/custom'
-
-  return makeDeleteRequest({
-    url,
-    token,
-    path,
-    statusCodeExpected
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getConfig,
-  getCustomConfig,
-  updateCustomConfig,
-  getAbout,
-  deleteCustomConfig,
-  updateCustomSubConfig,
-  getCustomConfigResolutions
-}
diff --git a/shared/extra-utils/server/contact-form-command.ts b/shared/extra-utils/server/contact-form-command.ts
new file mode 100644 (file)
index 0000000..0e8fd6d
--- /dev/null
@@ -0,0 +1,31 @@
+import { HttpStatusCode } from '@shared/models'
+import { ContactForm } from '../../models/server'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class ContactFormCommand extends AbstractCommand {
+
+  send (options: OverrideCommandOptions & {
+    fromEmail: string
+    fromName: string
+    subject: string
+    body: string
+  }) {
+    const path = '/api/v1/server/contact'
+
+    const body: ContactForm = {
+      fromEmail: options.fromEmail,
+      fromName: options.fromName,
+      subject: options.subject,
+      body: options.body
+    }
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: body,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/server/contact-form.ts b/shared/extra-utils/server/contact-form.ts
deleted file mode 100644 (file)
index 6c9232c..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as request from 'supertest'
-import { ContactForm } from '../../models/server'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function sendContactForm (options: {
-  url: string
-  fromEmail: string
-  fromName: string
-  subject: string
-  body: string
-  expectedStatus?: number
-}) {
-  const path = '/api/v1/server/contact'
-
-  const body: ContactForm = {
-    fromEmail: options.fromEmail,
-    fromName: options.fromName,
-    subject: options.subject,
-    body: options.body
-  }
-  return request(options.url)
-    .post(path)
-    .send(body)
-    .expect(options.expectedStatus || HttpStatusCode.NO_CONTENT_204)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  sendContactForm
-}
diff --git a/shared/extra-utils/server/debug-command.ts b/shared/extra-utils/server/debug-command.ts
new file mode 100644 (file)
index 0000000..3c5a785
--- /dev/null
@@ -0,0 +1,33 @@
+import { Debug, HttpStatusCode, SendDebugCommand } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class DebugCommand extends AbstractCommand {
+
+  getDebug (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/server/debug'
+
+    return this.getRequestBody<Debug>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  sendCommand (options: OverrideCommandOptions & {
+    body: SendDebugCommand
+  }) {
+    const { body } = options
+    const path = '/api/v1/server/debug/run-command'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: body,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/server/debug.ts b/shared/extra-utils/server/debug.ts
deleted file mode 100644 (file)
index f196812..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-import { makeGetRequest, makePostBodyRequest } from '../requests/requests'
-import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
-import { SendDebugCommand } from '@shared/models'
-
-function getDebug (url: string, token: string) {
-  const path = '/api/v1/server/debug'
-
-  return makeGetRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function sendDebugCommand (url: string, token: string, body: SendDebugCommand) {
-  const path = '/api/v1/server/debug/run-command'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    fields: body,
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getDebug,
-  sendDebugCommand
-}
diff --git a/shared/extra-utils/server/directories.ts b/shared/extra-utils/server/directories.ts
new file mode 100644 (file)
index 0000000..b6465cb
--- /dev/null
@@ -0,0 +1,34 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { expect } from 'chai'
+import { pathExists, readdir } from 'fs-extra'
+import { join } from 'path'
+import { root } from '@server/helpers/core-utils'
+import { PeerTubeServer } from './server'
+
+async function checkTmpIsEmpty (server: PeerTubeServer) {
+  await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
+
+  if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) {
+    await checkDirectoryIsEmpty(server, 'tmp/hls')
+  }
+}
+
+async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
+  const testDirectory = 'test' + server.internalServerNumber
+
+  const directoryPath = join(root(), testDirectory, directory)
+
+  const directoryExists = await pathExists(directoryPath)
+  expect(directoryExists).to.be.true
+
+  const files = await readdir(directoryPath)
+  const filtered = files.filter(f => exceptions.includes(f) === false)
+
+  expect(filtered).to.have.lengthOf(0)
+}
+
+export {
+  checkTmpIsEmpty,
+  checkDirectoryIsEmpty
+}
diff --git a/shared/extra-utils/server/follows-command.ts b/shared/extra-utils/server/follows-command.ts
new file mode 100644 (file)
index 0000000..01ef6f1
--- /dev/null
@@ -0,0 +1,139 @@
+import { pick } from '@shared/core-utils'
+import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+import { PeerTubeServer } from './server'
+
+export class FollowsCommand extends AbstractCommand {
+
+  getFollowers (options: OverrideCommandOptions & {
+    start: number
+    count: number
+    sort: string
+    search?: string
+    actorType?: ActivityPubActorType
+    state?: FollowState
+  }) {
+    const path = '/api/v1/server/followers'
+
+    const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])
+
+    return this.getRequestBody<ResultList<ActorFollow>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getFollowings (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+    search?: string
+    actorType?: ActivityPubActorType
+    state?: FollowState
+  } = {}) {
+    const path = '/api/v1/server/following'
+
+    const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])
+
+    return this.getRequestBody<ResultList<ActorFollow>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  follow (options: OverrideCommandOptions & {
+    hosts?: string[]
+    handles?: string[]
+  }) {
+    const path = '/api/v1/server/following'
+
+    const fields: ServerFollowCreate = {}
+
+    if (options.hosts) {
+      fields.hosts = options.hosts.map(f => f.replace(/^http:\/\//, ''))
+    }
+
+    if (options.handles) {
+      fields.handles = options.handles
+    }
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  async unfollow (options: OverrideCommandOptions & {
+    target: PeerTubeServer | string
+  }) {
+    const { target } = options
+
+    const handle = typeof target === 'string'
+      ? target
+      : target.host
+
+    const path = '/api/v1/server/following/' + handle
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  acceptFollower (options: OverrideCommandOptions & {
+    follower: string
+  }) {
+    const path = '/api/v1/server/followers/' + options.follower + '/accept'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  rejectFollower (options: OverrideCommandOptions & {
+    follower: string
+  }) {
+    const path = '/api/v1/server/followers/' + options.follower + '/reject'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  removeFollower (options: OverrideCommandOptions & {
+    follower: PeerTubeServer
+  }) {
+    const path = '/api/v1/server/followers/peertube@' + options.follower.host
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
index 6aae4a31dfe4462b92d06d8a4d9ef9c265a8d4ae..698238f29e89dff43f8d76c124011eace1136a56 100644 (file)
-import * as request from 'supertest'
-import { ServerInfo } from './servers'
 import { waitJobs } from './jobs'
-import { makePostBodyRequest } from '../requests/requests'
-import { ActivityPubActorType, FollowState } from '@shared/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { PeerTubeServer } from './server'
 
-function getFollowersListPaginationAndSort (options: {
-  url: string
-  start: number
-  count: number
-  sort: string
-  search?: string
-  actorType?: ActivityPubActorType
-  state?: FollowState
-}) {
-  const { url, start, count, sort, search, state, actorType } = options
-  const path = '/api/v1/server/followers'
-
-  const query = {
-    start,
-    count,
-    sort,
-    search,
-    state,
-    actorType
-  }
-
-  return request(url)
-    .get(path)
-    .query(query)
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function acceptFollower (url: string, token: string, follower: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/server/followers/' + follower + '/accept'
-
-  return makePostBodyRequest({
-    url,
-    token,
-    path,
-    statusCodeExpected
-  })
-}
-
-function rejectFollower (url: string, token: string, follower: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/server/followers/' + follower + '/reject'
-
-  return makePostBodyRequest({
-    url,
-    token,
-    path,
-    statusCodeExpected
-  })
-}
-
-function getFollowingListPaginationAndSort (options: {
-  url: string
-  start: number
-  count: number
-  sort: string
-  search?: string
-  actorType?: ActivityPubActorType
-  state?: FollowState
-}) {
-  const { url, start, count, sort, search, state, actorType } = options
-  const path = '/api/v1/server/following'
-
-  const query = {
-    start,
-    count,
-    sort,
-    search,
-    state,
-    actorType
-  }
-
-  return request(url)
-    .get(path)
-    .query(query)
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function follow (follower: string, following: string[], accessToken: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/server/following'
-
-  const followingHosts = following.map(f => f.replace(/^http:\/\//, ''))
-  return request(follower)
-    .post(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .send({ hosts: followingHosts })
-    .expect(expectedStatus)
-}
-
-async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/server/following/' + target.host
-
-  return request(url)
-    .delete(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(expectedStatus)
-}
-
-function removeFollower (url: string, accessToken: string, follower: ServerInfo, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/server/followers/peertube@' + follower.host
-
-  return request(url)
-    .delete(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(expectedStatus)
-}
-
-async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
+async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
   await Promise.all([
-    follow(server1.url, [ server2.url ], server1.accessToken),
-    follow(server2.url, [ server1.url ], server2.accessToken)
+    server1.follows.follow({ hosts: [ server2.url ] }),
+    server2.follows.follow({ hosts: [ server1.url ] })
   ])
 
   // Wait request propagation
@@ -132,12 +16,5 @@ async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
 // ---------------------------------------------------------------------------
 
 export {
-  getFollowersListPaginationAndSort,
-  getFollowingListPaginationAndSort,
-  unfollow,
-  removeFollower,
-  follow,
-  doubleFollow,
-  acceptFollower,
-  rejectFollower
+  doubleFollow
 }
diff --git a/shared/extra-utils/server/index.ts b/shared/extra-utils/server/index.ts
new file mode 100644 (file)
index 0000000..9055dfc
--- /dev/null
@@ -0,0 +1,15 @@
+export * from './config-command'
+export * from './contact-form-command'
+export * from './debug-command'
+export * from './directories'
+export * from './follows-command'
+export * from './follows'
+export * from './jobs'
+export * from './jobs-command'
+export * from './plugins-command'
+export * from './plugins'
+export * from './redundancy-command'
+export * from './server'
+export * from './servers-command'
+export * from './servers'
+export * from './stats-command'
diff --git a/shared/extra-utils/server/jobs-command.ts b/shared/extra-utils/server/jobs-command.ts
new file mode 100644 (file)
index 0000000..c4eb12d
--- /dev/null
@@ -0,0 +1,36 @@
+import { pick } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
+import { Job, JobState, JobType, ResultList } from '../../models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class JobsCommand extends AbstractCommand {
+
+  getJobsList (options: OverrideCommandOptions & {
+    state?: JobState
+    jobType?: JobType
+    start?: number
+    count?: number
+    sort?: string
+  } = {}) {
+    const path = this.buildJobsUrl(options.state)
+
+    const query = pick(options, [ 'start', 'count', 'sort', 'jobType' ])
+
+    return this.getRequestBody<ResultList<Job>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  private buildJobsUrl (state?: JobState) {
+    let path = '/api/v1/jobs'
+
+    if (state) path += '/' + state
+
+    return path
+  }
+}
index 763374e030e39323a0ef8983ebc437f57c186bfb..64a0353eba5b4ad79915bc67f2c0fc015d5df632 100644 (file)
@@ -1,66 +1,17 @@
-import * as request from 'supertest'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-import { getDebug, makeGetRequest } from '../../../shared/extra-utils'
-import { Job, JobState, JobType, ServerDebug } from '../../models'
-import { wait } from '../miscs/miscs'
-import { ServerInfo } from './servers'
 
-function buildJobsUrl (state?: JobState) {
-  let path = '/api/v1/jobs'
+import { JobState } from '../../models'
+import { wait } from '../miscs'
+import { PeerTubeServer } from './server'
 
-  if (state) path += '/' + state
-
-  return path
-}
-
-function getJobsList (url: string, accessToken: string, state?: JobState) {
-  const path = buildJobsUrl(state)
-
-  return request(url)
-    .get(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getJobsListPaginationAndSort (options: {
-  url: string
-  accessToken: string
-  start: number
-  count: number
-  sort: string
-  state?: JobState
-  jobType?: JobType
-}) {
-  const { url, accessToken, state, start, count, sort, jobType } = options
-  const path = buildJobsUrl(state)
-
-  const query = {
-    start,
-    count,
-    sort,
-    jobType
-  }
-
-  return makeGetRequest({
-    url,
-    path,
-    token: accessToken,
-    statusCodeExpected: HttpStatusCode.OK_200,
-    query
-  })
-}
-
-async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
+async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer) {
   const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT
     ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10)
     : 250
 
-  let servers: ServerInfo[]
+  let servers: PeerTubeServer[]
 
-  if (Array.isArray(serversArg) === false) servers = [ serversArg as ServerInfo ]
-  else servers = serversArg as ServerInfo[]
+  if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ]
+  else servers = serversArg as PeerTubeServer[]
 
   const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
   const repeatableJobs = [ 'videos-views', 'activitypub-cleaner' ]
@@ -72,15 +23,13 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
     // Check if each server has pending request
     for (const server of servers) {
       for (const state of states) {
-        const p = getJobsListPaginationAndSort({
-          url: server.url,
-          accessToken: server.accessToken,
-          state: state,
+        const p = server.jobs.getJobsList({
+          state,
           start: 0,
           count: 10,
           sort: '-createdAt'
-        }).then(res => res.body.data)
-          .then((jobs: Job[]) => jobs.filter(j => !repeatableJobs.includes(j.type)))
+        }).then(body => body.data)
+          .then(jobs => jobs.filter(j => !repeatableJobs.includes(j.type)))
           .then(jobs => {
             if (jobs.length !== 0) {
               pendingRequests = true
@@ -90,9 +39,8 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
         tasks.push(p)
       }
 
-      const p = getDebug(server.url, server.accessToken)
-        .then(res => res.body)
-        .then((obj: ServerDebug) => {
+      const p = server.debug.getDebug()
+        .then(obj => {
           if (obj.activityPubMessagesWaiting !== 0) {
             pendingRequests = true
           }
@@ -123,7 +71,5 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
 // ---------------------------------------------------------------------------
 
 export {
-  getJobsList,
-  waitJobs,
-  getJobsListPaginationAndSort
+  waitJobs
 }
diff --git a/shared/extra-utils/server/plugins-command.ts b/shared/extra-utils/server/plugins-command.ts
new file mode 100644 (file)
index 0000000..b944475
--- /dev/null
@@ -0,0 +1,256 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { readJSON, writeJSON } from 'fs-extra'
+import { join } from 'path'
+import { root } from '@server/helpers/core-utils'
+import {
+  HttpStatusCode,
+  PeerTubePlugin,
+  PeerTubePluginIndex,
+  PeertubePluginIndexList,
+  PluginPackageJson,
+  PluginTranslation,
+  PluginType,
+  PublicServerSetting,
+  RegisteredServerSettings,
+  ResultList
+} from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class PluginsCommand extends AbstractCommand {
+
+  static getPluginTestPath (suffix = '') {
+    return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
+  }
+
+  list (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+    pluginType?: PluginType
+    uninstalled?: boolean
+  }) {
+    const { start, count, sort, pluginType, uninstalled } = options
+    const path = '/api/v1/plugins'
+
+    return this.getRequestBody<ResultList<PeerTubePlugin>>({
+      ...options,
+
+      path,
+      query: {
+        start,
+        count,
+        sort,
+        pluginType,
+        uninstalled
+      },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listAvailable (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+    pluginType?: PluginType
+    currentPeerTubeEngine?: string
+    search?: string
+    expectedStatus?: HttpStatusCode
+  }) {
+    const { start, count, sort, pluginType, search, currentPeerTubeEngine } = options
+    const path = '/api/v1/plugins/available'
+
+    const query: PeertubePluginIndexList = {
+      start,
+      count,
+      sort,
+      pluginType,
+      currentPeerTubeEngine,
+      search
+    }
+
+    return this.getRequestBody<ResultList<PeerTubePluginIndex>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  get (options: OverrideCommandOptions & {
+    npmName: string
+  }) {
+    const path = '/api/v1/plugins/' + options.npmName
+
+    return this.getRequestBody<PeerTubePlugin>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  updateSettings (options: OverrideCommandOptions & {
+    npmName: string
+    settings: any
+  }) {
+    const { npmName, settings } = options
+    const path = '/api/v1/plugins/' + npmName + '/settings'
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: { settings },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  getRegisteredSettings (options: OverrideCommandOptions & {
+    npmName: string
+  }) {
+    const path = '/api/v1/plugins/' + options.npmName + '/registered-settings'
+
+    return this.getRequestBody<RegisteredServerSettings>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getPublicSettings (options: OverrideCommandOptions & {
+    npmName: string
+  }) {
+    const { npmName } = options
+    const path = '/api/v1/plugins/' + npmName + '/public-settings'
+
+    return this.getRequestBody<PublicServerSetting>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getTranslations (options: OverrideCommandOptions & {
+    locale: string
+  }) {
+    const { locale } = options
+    const path = '/plugins/translations/' + locale + '.json'
+
+    return this.getRequestBody<PluginTranslation>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  install (options: OverrideCommandOptions & {
+    path?: string
+    npmName?: string
+  }) {
+    const { npmName, path } = options
+    const apiPath = '/api/v1/plugins/install'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path: apiPath,
+      fields: { npmName, path },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  update (options: OverrideCommandOptions & {
+    path?: string
+    npmName?: string
+  }) {
+    const { npmName, path } = options
+    const apiPath = '/api/v1/plugins/update'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path: apiPath,
+      fields: { npmName, path },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  uninstall (options: OverrideCommandOptions & {
+    npmName: string
+  }) {
+    const { npmName } = options
+    const apiPath = '/api/v1/plugins/uninstall'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path: apiPath,
+      fields: { npmName },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  getCSS (options: OverrideCommandOptions = {}) {
+    const path = '/plugins/global.css'
+
+    return this.getRequestText({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getExternalAuth (options: OverrideCommandOptions & {
+    npmName: string
+    npmVersion: string
+    authName: string
+    query?: any
+  }) {
+    const { npmName, npmVersion, authName, query } = options
+
+    const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
+
+    return this.getRequest({
+      ...options,
+
+      path,
+      query,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200,
+      redirects: 0
+    })
+  }
+
+  updatePackageJSON (npmName: string, json: any) {
+    const path = this.getPackageJSONPath(npmName)
+
+    return writeJSON(path, json)
+  }
+
+  getPackageJSON (npmName: string): Promise<PluginPackageJson> {
+    const path = this.getPackageJSONPath(npmName)
+
+    return readJSON(path)
+  }
+
+  private getPackageJSONPath (npmName: string) {
+    return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json'))
+  }
+}
index d53e5b3828090165b0524711ee7986686c2837ab..0f5fabd5abab9b3fedcbd644ba86fda26a069a8d 100644 (file)
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
-import { readJSON, writeJSON } from 'fs-extra'
-import { join } from 'path'
-import { RegisteredServerSettings } from '@shared/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-import { PeertubePluginIndexList } from '../../models/plugins/plugin-index/peertube-plugin-index-list.model'
-import { PluginType } from '../../models/plugins/plugin.type'
-import { buildServerDirectory, root } from '../miscs/miscs'
-import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
-import { ServerInfo } from './servers'
+import { PeerTubeServer } from '../server/server'
 
-function listPlugins (parameters: {
-  url: string
-  accessToken: string
-  start?: number
-  count?: number
-  sort?: string
-  pluginType?: PluginType
-  uninstalled?: boolean
-  expectedStatus?: HttpStatusCode
-}) {
-  const { url, accessToken, start, count, sort, pluginType, uninstalled, expectedStatus = HttpStatusCode.OK_200 } = parameters
-  const path = '/api/v1/plugins'
-
-  return makeGetRequest({
-    url,
-    path,
-    token: accessToken,
-    query: {
-      start,
-      count,
-      sort,
-      pluginType,
-      uninstalled
-    },
-    statusCodeExpected: expectedStatus
-  })
-}
-
-function listAvailablePlugins (parameters: {
-  url: string
-  accessToken: string
-  start?: number
-  count?: number
-  sort?: string
-  pluginType?: PluginType
-  currentPeerTubeEngine?: string
-  search?: string
-  expectedStatus?: HttpStatusCode
-}) {
-  const {
-    url,
-    accessToken,
-    start,
-    count,
-    sort,
-    pluginType,
-    search,
-    currentPeerTubeEngine,
-    expectedStatus = HttpStatusCode.OK_200
-  } = parameters
-  const path = '/api/v1/plugins/available'
-
-  const query: PeertubePluginIndexList = {
-    start,
-    count,
-    sort,
-    pluginType,
-    currentPeerTubeEngine,
-    search
-  }
-
-  return makeGetRequest({
-    url,
-    path,
-    token: accessToken,
-    query,
-    statusCodeExpected: expectedStatus
-  })
-}
-
-function getPlugin (parameters: {
-  url: string
-  accessToken: string
-  npmName: string
-  expectedStatus?: HttpStatusCode
-}) {
-  const { url, accessToken, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
-  const path = '/api/v1/plugins/' + npmName
-
-  return makeGetRequest({
-    url,
-    path,
-    token: accessToken,
-    statusCodeExpected: expectedStatus
-  })
-}
-
-function updatePluginSettings (parameters: {
-  url: string
-  accessToken: string
-  npmName: string
-  settings: any
-  expectedStatus?: HttpStatusCode
-}) {
-  const { url, accessToken, npmName, settings, expectedStatus = HttpStatusCode.NO_CONTENT_204 } = parameters
-  const path = '/api/v1/plugins/' + npmName + '/settings'
-
-  return makePutBodyRequest({
-    url,
-    path,
-    token: accessToken,
-    fields: { settings },
-    statusCodeExpected: expectedStatus
-  })
-}
-
-function getPluginRegisteredSettings (parameters: {
-  url: string
-  accessToken: string
-  npmName: string
-  expectedStatus?: HttpStatusCode
-}) {
-  const { url, accessToken, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
-  const path = '/api/v1/plugins/' + npmName + '/registered-settings'
-
-  return makeGetRequest({
-    url,
-    path,
-    token: accessToken,
-    statusCodeExpected: expectedStatus
-  })
-}
-
-async function testHelloWorldRegisteredSettings (server: ServerInfo) {
-  const res = await getPluginRegisteredSettings({
-    url: server.url,
-    accessToken: server.accessToken,
-    npmName: 'peertube-plugin-hello-world'
-  })
-
-  const registeredSettings = (res.body as RegisteredServerSettings).registeredSettings
+async function testHelloWorldRegisteredSettings (server: PeerTubeServer) {
+  const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' })
 
+  const registeredSettings = body.registeredSettings
   expect(registeredSettings).to.have.length.at.least(1)
 
   const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name')
   expect(adminNameSettings).to.not.be.undefined
 }
 
-function getPublicSettings (parameters: {
-  url: string
-  npmName: string
-  expectedStatus?: HttpStatusCode
-}) {
-  const { url, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
-  const path = '/api/v1/plugins/' + npmName + '/public-settings'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: expectedStatus
-  })
-}
-
-function getPluginTranslations (parameters: {
-  url: string
-  locale: string
-  expectedStatus?: HttpStatusCode
-}) {
-  const { url, locale, expectedStatus = HttpStatusCode.OK_200 } = parameters
-  const path = '/plugins/translations/' + locale + '.json'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: expectedStatus
-  })
-}
-
-function installPlugin (parameters: {
-  url: string
-  accessToken: string
-  path?: string
-  npmName?: string
-  expectedStatus?: HttpStatusCode
-}) {
-  const { url, accessToken, npmName, path, expectedStatus = HttpStatusCode.OK_200 } = parameters
-  const apiPath = '/api/v1/plugins/install'
-
-  return makePostBodyRequest({
-    url,
-    path: apiPath,
-    token: accessToken,
-    fields: { npmName, path },
-    statusCodeExpected: expectedStatus
-  })
-}
-
-function updatePlugin (parameters: {
-  url: string
-  accessToken: string
-  path?: string
-  npmName?: string
-  expectedStatus?: HttpStatusCode
-}) {
-  const { url, accessToken, npmName, path, expectedStatus = HttpStatusCode.OK_200 } = parameters
-  const apiPath = '/api/v1/plugins/update'
-
-  return makePostBodyRequest({
-    url,
-    path: apiPath,
-    token: accessToken,
-    fields: { npmName, path },
-    statusCodeExpected: expectedStatus
-  })
-}
-
-function uninstallPlugin (parameters: {
-  url: string
-  accessToken: string
-  npmName: string
-  expectedStatus?: HttpStatusCode
-}) {
-  const { url, accessToken, npmName, expectedStatus = HttpStatusCode.NO_CONTENT_204 } = parameters
-  const apiPath = '/api/v1/plugins/uninstall'
-
-  return makePostBodyRequest({
-    url,
-    path: apiPath,
-    token: accessToken,
-    fields: { npmName },
-    statusCodeExpected: expectedStatus
-  })
-}
-
-function getPluginsCSS (url: string) {
-  const path = '/plugins/global.css'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getPackageJSONPath (server: ServerInfo, npmName: string) {
-  return buildServerDirectory(server, join('plugins', 'node_modules', npmName, 'package.json'))
-}
-
-function updatePluginPackageJSON (server: ServerInfo, npmName: string, json: any) {
-  const path = getPackageJSONPath(server, npmName)
-
-  return writeJSON(path, json)
-}
-
-function getPluginPackageJSON (server: ServerInfo, npmName: string) {
-  const path = getPackageJSONPath(server, npmName)
-
-  return readJSON(path)
-}
-
-function getPluginTestPath (suffix = '') {
-  return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
-}
-
-function getExternalAuth (options: {
-  url: string
-  npmName: string
-  npmVersion: string
-  authName: string
-  query?: any
-  statusCodeExpected?: HttpStatusCode
-}) {
-  const { url, npmName, npmVersion, authName, statusCodeExpected, query } = options
-
-  const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
-
-  return makeGetRequest({
-    url,
-    path,
-    query,
-    statusCodeExpected: statusCodeExpected || HttpStatusCode.OK_200,
-    redirects: 0
-  })
-}
-
 export {
-  listPlugins,
-  listAvailablePlugins,
-  installPlugin,
-  getPluginTranslations,
-  getPluginsCSS,
-  updatePlugin,
-  getPlugin,
-  uninstallPlugin,
-  testHelloWorldRegisteredSettings,
-  updatePluginSettings,
-  getPluginRegisteredSettings,
-  getPackageJSONPath,
-  updatePluginPackageJSON,
-  getPluginPackageJSON,
-  getPluginTestPath,
-  getPublicSettings,
-  getExternalAuth
+  testHelloWorldRegisteredSettings
 }
diff --git a/shared/extra-utils/server/redundancy-command.ts b/shared/extra-utils/server/redundancy-command.ts
new file mode 100644 (file)
index 0000000..e7a8b3c
--- /dev/null
@@ -0,0 +1,80 @@
+import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class RedundancyCommand extends AbstractCommand {
+
+  updateRedundancy (options: OverrideCommandOptions & {
+    host: string
+    redundancyAllowed: boolean
+  }) {
+    const { host, redundancyAllowed } = options
+    const path = '/api/v1/server/redundancy/' + host
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: { redundancyAllowed },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  listVideos (options: OverrideCommandOptions & {
+    target: VideoRedundanciesTarget
+    start?: number
+    count?: number
+    sort?: string
+  }) {
+    const path = '/api/v1/server/redundancy/videos'
+
+    const { target, start, count, sort } = options
+
+    return this.getRequestBody<ResultList<VideoRedundancy>>({
+      ...options,
+
+      path,
+
+      query: {
+        start: start ?? 0,
+        count: count ?? 5,
+        sort: sort ?? 'name',
+        target
+      },
+
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  addVideo (options: OverrideCommandOptions & {
+    videoId: number
+  }) {
+    const path = '/api/v1/server/redundancy/videos'
+    const { videoId } = options
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { videoId },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  removeVideo (options: OverrideCommandOptions & {
+    redundancyId: number
+  }) {
+    const { redundancyId } = options
+    const path = '/api/v1/server/redundancy/videos/' + redundancyId
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/server/redundancy.ts b/shared/extra-utils/server/redundancy.ts
deleted file mode 100644 (file)
index b83815a..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
-import { VideoRedundanciesTarget } from '@shared/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function updateRedundancy (
-  url: string,
-  accessToken: string,
-  host: string,
-  redundancyAllowed: boolean,
-  expectedStatus = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/server/redundancy/' + host
-
-  return makePutBodyRequest({
-    url,
-    path,
-    token: accessToken,
-    fields: { redundancyAllowed },
-    statusCodeExpected: expectedStatus
-  })
-}
-
-function listVideoRedundancies (options: {
-  url: string
-  accessToken: string
-  target: VideoRedundanciesTarget
-  start?: number
-  count?: number
-  sort?: string
-  statusCodeExpected?: HttpStatusCode
-}) {
-  const path = '/api/v1/server/redundancy/videos'
-
-  const { url, accessToken, target, statusCodeExpected, start, count, sort } = options
-
-  return makeGetRequest({
-    url,
-    token: accessToken,
-    path,
-    query: {
-      start: start ?? 0,
-      count: count ?? 5,
-      sort: sort ?? 'name',
-      target
-    },
-    statusCodeExpected: statusCodeExpected || HttpStatusCode.OK_200
-  })
-}
-
-function addVideoRedundancy (options: {
-  url: string
-  accessToken: string
-  videoId: number
-}) {
-  const path = '/api/v1/server/redundancy/videos'
-  const { url, accessToken, videoId } = options
-
-  return makePostBodyRequest({
-    url,
-    token: accessToken,
-    path,
-    fields: { videoId },
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-function removeVideoRedundancy (options: {
-  url: string
-  accessToken: string
-  redundancyId: number
-}) {
-  const { url, accessToken, redundancyId } = options
-  const path = '/api/v1/server/redundancy/videos/' + redundancyId
-
-  return makeDeleteRequest({
-    url,
-    token: accessToken,
-    path,
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-export {
-  updateRedundancy,
-  listVideoRedundancies,
-  addVideoRedundancy,
-  removeVideoRedundancy
-}
diff --git a/shared/extra-utils/server/server.ts b/shared/extra-utils/server/server.ts
new file mode 100644 (file)
index 0000000..3c335b8
--- /dev/null
@@ -0,0 +1,369 @@
+import { ChildProcess, fork } from 'child_process'
+import { copy } from 'fs-extra'
+import { join } from 'path'
+import { root } from '@server/helpers/core-utils'
+import { randomInt } from '@shared/core-utils'
+import { Video, VideoChannel, VideoCreateResult, VideoDetails } from '../../models/videos'
+import { BulkCommand } from '../bulk'
+import { CLICommand } from '../cli'
+import { CustomPagesCommand } from '../custom-pages'
+import { FeedCommand } from '../feeds'
+import { LogsCommand } from '../logs'
+import { parallelTests, SQLCommand } from '../miscs'
+import { AbusesCommand } from '../moderation'
+import { OverviewsCommand } from '../overviews'
+import { SearchCommand } from '../search'
+import { SocketIOCommand } from '../socket'
+import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users'
+import {
+  BlacklistCommand,
+  CaptionsCommand,
+  ChangeOwnershipCommand,
+  ChannelsCommand,
+  HistoryCommand,
+  ImportsCommand,
+  LiveCommand,
+  PlaylistsCommand,
+  ServicesCommand,
+  StreamingPlaylistsCommand,
+  VideosCommand
+} from '../videos'
+import { CommentsCommand } from '../videos/comments-command'
+import { ConfigCommand } from './config-command'
+import { ContactFormCommand } from './contact-form-command'
+import { DebugCommand } from './debug-command'
+import { FollowsCommand } from './follows-command'
+import { JobsCommand } from './jobs-command'
+import { PluginsCommand } from './plugins-command'
+import { RedundancyCommand } from './redundancy-command'
+import { ServersCommand } from './servers-command'
+import { StatsCommand } from './stats-command'
+
+export type RunServerOptions = {
+  hideLogs?: boolean
+  nodeArgs?: string[]
+  peertubeArgs?: string[]
+}
+
+export class PeerTubeServer {
+  app?: ChildProcess
+
+  url: string
+  host?: string
+  hostname?: string
+  port?: number
+
+  rtmpPort?: number
+
+  parallel?: boolean
+  internalServerNumber: number
+
+  serverNumber?: number
+  customConfigFile?: string
+
+  store?: {
+    client?: {
+      id?: string
+      secret?: string
+    }
+
+    user?: {
+      username: string
+      password: string
+      email?: string
+    }
+
+    channel?: VideoChannel
+
+    video?: Video
+    videoCreated?: VideoCreateResult
+    videoDetails?: VideoDetails
+
+    videos?: { id: number, uuid: string }[]
+  }
+
+  accessToken?: string
+  refreshToken?: string
+
+  bulk?: BulkCommand
+  cli?: CLICommand
+  customPage?: CustomPagesCommand
+  feed?: FeedCommand
+  logs?: LogsCommand
+  abuses?: AbusesCommand
+  overviews?: OverviewsCommand
+  search?: SearchCommand
+  contactForm?: ContactFormCommand
+  debug?: DebugCommand
+  follows?: FollowsCommand
+  jobs?: JobsCommand
+  plugins?: PluginsCommand
+  redundancy?: RedundancyCommand
+  stats?: StatsCommand
+  config?: ConfigCommand
+  socketIO?: SocketIOCommand
+  accounts?: AccountsCommand
+  blocklist?: BlocklistCommand
+  subscriptions?: SubscriptionsCommand
+  live?: LiveCommand
+  services?: ServicesCommand
+  blacklist?: BlacklistCommand
+  captions?: CaptionsCommand
+  changeOwnership?: ChangeOwnershipCommand
+  playlists?: PlaylistsCommand
+  history?: HistoryCommand
+  imports?: ImportsCommand
+  streamingPlaylists?: StreamingPlaylistsCommand
+  channels?: ChannelsCommand
+  comments?: CommentsCommand
+  sql?: SQLCommand
+  notifications?: NotificationsCommand
+  servers?: ServersCommand
+  login?: LoginCommand
+  users?: UsersCommand
+  videos?: VideosCommand
+
+  constructor (options: { serverNumber: number } | { url: string }) {
+    if ((options as any).url) {
+      this.setUrl((options as any).url)
+    } else {
+      this.setServerNumber((options as any).serverNumber)
+    }
+
+    this.store = {
+      client: {
+        id: null,
+        secret: null
+      },
+      user: {
+        username: null,
+        password: null
+      }
+    }
+
+    this.assignCommands()
+  }
+
+  setServerNumber (serverNumber: number) {
+    this.serverNumber = serverNumber
+
+    this.parallel = parallelTests()
+
+    this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber
+    this.rtmpPort = this.parallel ? this.randomRTMP() : 1936
+    this.port = 9000 + this.internalServerNumber
+
+    this.url = `http://localhost:${this.port}`
+    this.host = `localhost:${this.port}`
+    this.hostname = 'localhost'
+  }
+
+  setUrl (url: string) {
+    const parsed = new URL(url)
+
+    this.url = url
+    this.host = parsed.host
+    this.hostname = parsed.hostname
+    this.port = parseInt(parsed.port)
+  }
+
+  async flushAndRun (configOverride?: Object, options: RunServerOptions = {}) {
+    await ServersCommand.flushTests(this.internalServerNumber)
+
+    return this.run(configOverride, options)
+  }
+
+  async run (configOverrideArg?: any, options: RunServerOptions = {}) {
+    // These actions are async so we need to be sure that they have both been done
+    const serverRunString = {
+      'HTTP server listening': false
+    }
+    const key = 'Database peertube_test' + this.internalServerNumber + ' is ready'
+    serverRunString[key] = false
+
+    const regexps = {
+      client_id: 'Client id: (.+)',
+      client_secret: 'Client secret: (.+)',
+      user_username: 'Username: (.+)',
+      user_password: 'User password: (.+)'
+    }
+
+    await this.assignCustomConfigFile()
+
+    const configOverride = this.buildConfigOverride()
+
+    if (configOverrideArg !== undefined) {
+      Object.assign(configOverride, configOverrideArg)
+    }
+
+    // Share the environment
+    const env = Object.create(process.env)
+    env['NODE_ENV'] = 'test'
+    env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString()
+    env['NODE_CONFIG'] = JSON.stringify(configOverride)
+
+    const forkOptions = {
+      silent: true,
+      env,
+      detached: true,
+      execArgv: options.nodeArgs || []
+    }
+
+    return new Promise<void>(res => {
+      const self = this
+
+      this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions)
+      this.app.stdout.on('data', function onStdout (data) {
+        let dontContinue = false
+
+        // Capture things if we want to
+        for (const key of Object.keys(regexps)) {
+          const regexp = regexps[key]
+          const matches = data.toString().match(regexp)
+          if (matches !== null) {
+            if (key === 'client_id') self.store.client.id = matches[1]
+            else if (key === 'client_secret') self.store.client.secret = matches[1]
+            else if (key === 'user_username') self.store.user.username = matches[1]
+            else if (key === 'user_password') self.store.user.password = matches[1]
+          }
+        }
+
+        // Check if all required sentences are here
+        for (const key of Object.keys(serverRunString)) {
+          if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
+          if (serverRunString[key] === false) dontContinue = true
+        }
+
+        // If no, there is maybe one thing not already initialized (client/user credentials generation...)
+        if (dontContinue === true) return
+
+        if (options.hideLogs === false) {
+          console.log(data.toString())
+        } else {
+          self.app.stdout.removeListener('data', onStdout)
+        }
+
+        process.on('exit', () => {
+          try {
+            process.kill(self.app.pid)
+          } catch { /* empty */ }
+        })
+
+        res()
+      })
+    })
+  }
+
+  async kill () {
+    if (!this.app) return
+
+    await this.sql.cleanup()
+
+    process.kill(-this.app.pid)
+
+    this.app = null
+  }
+
+  private randomServer () {
+    const low = 10
+    const high = 10000
+
+    return randomInt(low, high)
+  }
+
+  private randomRTMP () {
+    const low = 1900
+    const high = 2100
+
+    return randomInt(low, high)
+  }
+
+  private async assignCustomConfigFile () {
+    if (this.internalServerNumber === this.serverNumber) return
+
+    const basePath = join(root(), 'config')
+
+    const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`)
+    await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile)
+
+    this.customConfigFile = tmpConfigFile
+  }
+
+  private buildConfigOverride () {
+    if (!this.parallel) return {}
+
+    return {
+      listen: {
+        port: this.port
+      },
+      webserver: {
+        port: this.port
+      },
+      database: {
+        suffix: '_test' + this.internalServerNumber
+      },
+      storage: {
+        tmp: `test${this.internalServerNumber}/tmp/`,
+        avatars: `test${this.internalServerNumber}/avatars/`,
+        videos: `test${this.internalServerNumber}/videos/`,
+        streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`,
+        redundancy: `test${this.internalServerNumber}/redundancy/`,
+        logs: `test${this.internalServerNumber}/logs/`,
+        previews: `test${this.internalServerNumber}/previews/`,
+        thumbnails: `test${this.internalServerNumber}/thumbnails/`,
+        torrents: `test${this.internalServerNumber}/torrents/`,
+        captions: `test${this.internalServerNumber}/captions/`,
+        cache: `test${this.internalServerNumber}/cache/`,
+        plugins: `test${this.internalServerNumber}/plugins/`
+      },
+      admin: {
+        email: `admin${this.internalServerNumber}@example.com`
+      },
+      live: {
+        rtmp: {
+          port: this.rtmpPort
+        }
+      }
+    }
+  }
+
+  private assignCommands () {
+    this.bulk = new BulkCommand(this)
+    this.cli = new CLICommand(this)
+    this.customPage = new CustomPagesCommand(this)
+    this.feed = new FeedCommand(this)
+    this.logs = new LogsCommand(this)
+    this.abuses = new AbusesCommand(this)
+    this.overviews = new OverviewsCommand(this)
+    this.search = new SearchCommand(this)
+    this.contactForm = new ContactFormCommand(this)
+    this.debug = new DebugCommand(this)
+    this.follows = new FollowsCommand(this)
+    this.jobs = new JobsCommand(this)
+    this.plugins = new PluginsCommand(this)
+    this.redundancy = new RedundancyCommand(this)
+    this.stats = new StatsCommand(this)
+    this.config = new ConfigCommand(this)
+    this.socketIO = new SocketIOCommand(this)
+    this.accounts = new AccountsCommand(this)
+    this.blocklist = new BlocklistCommand(this)
+    this.subscriptions = new SubscriptionsCommand(this)
+    this.live = new LiveCommand(this)
+    this.services = new ServicesCommand(this)
+    this.blacklist = new BlacklistCommand(this)
+    this.captions = new CaptionsCommand(this)
+    this.changeOwnership = new ChangeOwnershipCommand(this)
+    this.playlists = new PlaylistsCommand(this)
+    this.history = new HistoryCommand(this)
+    this.imports = new ImportsCommand(this)
+    this.streamingPlaylists = new StreamingPlaylistsCommand(this)
+    this.channels = new ChannelsCommand(this)
+    this.comments = new CommentsCommand(this)
+    this.sql = new SQLCommand(this)
+    this.notifications = new NotificationsCommand(this)
+    this.servers = new ServersCommand(this)
+    this.login = new LoginCommand(this)
+    this.users = new UsersCommand(this)
+    this.videos = new VideosCommand(this)
+  }
+}
diff --git a/shared/extra-utils/server/servers-command.ts b/shared/extra-utils/server/servers-command.ts
new file mode 100644 (file)
index 0000000..40a11e8
--- /dev/null
@@ -0,0 +1,88 @@
+import { exec } from 'child_process'
+import { copy, ensureDir, readFile, remove } from 'fs-extra'
+import { basename, join } from 'path'
+import { root } from '@server/helpers/core-utils'
+import { HttpStatusCode } from '@shared/models'
+import { getFileSize, isGithubCI, wait } from '../miscs'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class ServersCommand extends AbstractCommand {
+
+  static flushTests (internalServerNumber: number) {
+    return new Promise<void>((res, rej) => {
+      const suffix = ` -- ${internalServerNumber}`
+
+      return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
+        if (err || stderr) return rej(err || new Error(stderr))
+
+        return res()
+      })
+    })
+  }
+
+  ping (options: OverrideCommandOptions = {}) {
+    return this.getRequestBody({
+      ...options,
+
+      path: '/api/v1/ping',
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  async cleanupTests () {
+    const p: Promise<any>[] = []
+
+    if (isGithubCI()) {
+      await ensureDir('artifacts')
+
+      const origin = this.buildDirectory('logs/peertube.log')
+      const destname = `peertube-${this.server.internalServerNumber}.log`
+      console.log('Saving logs %s.', destname)
+
+      await copy(origin, join('artifacts', destname))
+    }
+
+    if (this.server.parallel) {
+      p.push(ServersCommand.flushTests(this.server.internalServerNumber))
+    }
+
+    if (this.server.customConfigFile) {
+      p.push(remove(this.server.customConfigFile))
+    }
+
+    return p
+  }
+
+  async waitUntilLog (str: string, count = 1, strictCount = true) {
+    const logfile = this.server.servers.buildDirectory('logs/peertube.log')
+
+    while (true) {
+      const buf = await readFile(logfile)
+
+      const matches = buf.toString().match(new RegExp(str, 'g'))
+      if (matches && matches.length === count) return
+      if (matches && strictCount === false && matches.length >= count) return
+
+      await wait(1000)
+    }
+  }
+
+  buildDirectory (directory: string) {
+    return join(root(), 'test' + this.server.internalServerNumber, directory)
+  }
+
+  buildWebTorrentFilePath (fileUrl: string) {
+    return this.buildDirectory(join('videos', basename(fileUrl)))
+  }
+
+  buildFragmentedFilePath (videoUUID: string, fileUrl: string) {
+    return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl)))
+  }
+
+  async getServerFileSize (subPath: string) {
+    const path = this.server.servers.buildDirectory(subPath)
+
+    return getFileSize(path)
+  }
+}
index 28e431e94efee2ba7c9e04f58c2bb6eab67523bf..f0622feb067bd4813b0580338736027626850471 100644 (file)
-/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
+import { ensureDir } from 'fs-extra'
+import { isGithubCI } from '../miscs'
+import { PeerTubeServer, RunServerOptions } from './server'
 
-import { expect } from 'chai'
-import { ChildProcess, exec, fork } from 'child_process'
-import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra'
-import { join } from 'path'
-import { randomInt } from '../../core-utils/miscs/miscs'
-import { VideoChannel } from '../../models/videos'
-import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs'
-import { makeGetRequest } from '../requests/requests'
+async function createSingleServer (serverNumber: number, configOverride?: Object, options: RunServerOptions = {}) {
+  const server = new PeerTubeServer({ serverNumber })
 
-interface ServerInfo {
-  app: ChildProcess
-
-  url: string
-  host: string
-  hostname: string
-  port: number
-
-  rtmpPort: number
-
-  parallel: boolean
-  internalServerNumber: number
-  serverNumber: number
-
-  client: {
-    id: string
-    secret: string
-  }
-
-  user: {
-    username: string
-    password: string
-    email?: string
-  }
-
-  customConfigFile?: string
-
-  accessToken?: string
-  refreshToken?: string
-  videoChannel?: VideoChannel
-
-  video?: {
-    id: number
-    uuid: string
-    shortUUID: string
-    name?: string
-    url?: string
-
-    account?: {
-      name: string
-    }
-
-    embedPath?: string
-  }
-
-  remoteVideo?: {
-    id: number
-    uuid: string
-  }
-
-  videos?: { id: number, uuid: string }[]
-}
-
-function parallelTests () {
-  return process.env.MOCHA_PARALLEL === 'true'
-}
-
-function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
-  const apps = []
-  let i = 0
-
-  return new Promise<ServerInfo[]>(res => {
-    function anotherServerDone (serverNumber, app) {
-      apps[serverNumber - 1] = app
-      i++
-      if (i === totalServers) {
-        return res(apps)
-      }
-    }
-
-    for (let j = 1; j <= totalServers; j++) {
-      flushAndRunServer(j, configOverride).then(app => anotherServerDone(j, app))
-    }
-  })
-}
-
-function flushTests (serverNumber?: number) {
-  return new Promise<void>((res, rej) => {
-    const suffix = serverNumber ? ` -- ${serverNumber}` : ''
-
-    return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
-      if (err || stderr) return rej(err || new Error(stderr))
-
-      return res()
-    })
-  })
-}
-
-function randomServer () {
-  const low = 10
-  const high = 10000
-
-  return randomInt(low, high)
-}
-
-function randomRTMP () {
-  const low = 1900
-  const high = 2100
-
-  return randomInt(low, high)
-}
-
-type RunServerOptions = {
-  hideLogs?: boolean
-  execArgv?: string[]
-}
-
-async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = [], options: RunServerOptions = {}) {
-  const parallel = parallelTests()
-
-  const internalServerNumber = parallel ? randomServer() : serverNumber
-  const rtmpPort = parallel ? randomRTMP() : 1936
-  const port = 9000 + internalServerNumber
-
-  await flushTests(internalServerNumber)
-
-  const server: ServerInfo = {
-    app: null,
-    port,
-    internalServerNumber,
-    rtmpPort,
-    parallel,
-    serverNumber,
-    url: `http://localhost:${port}`,
-    host: `localhost:${port}`,
-    hostname: 'localhost',
-    client: {
-      id: null,
-      secret: null
-    },
-    user: {
-      username: null,
-      password: null
-    }
-  }
-
-  return runServer(server, configOverride, args, options)
-}
-
-async function runServer (server: ServerInfo, configOverrideArg?: any, args = [], options: RunServerOptions = {}) {
-  // These actions are async so we need to be sure that they have both been done
-  const serverRunString = {
-    'HTTP server listening': false
-  }
-  const key = 'Database peertube_test' + server.internalServerNumber + ' is ready'
-  serverRunString[key] = false
-
-  const regexps = {
-    client_id: 'Client id: (.+)',
-    client_secret: 'Client secret: (.+)',
-    user_username: 'Username: (.+)',
-    user_password: 'User password: (.+)'
-  }
-
-  if (server.internalServerNumber !== server.serverNumber) {
-    const basePath = join(root(), 'config')
-
-    const tmpConfigFile = join(basePath, `test-${server.internalServerNumber}.yaml`)
-    await copy(join(basePath, `test-${server.serverNumber}.yaml`), tmpConfigFile)
-
-    server.customConfigFile = tmpConfigFile
-  }
-
-  const configOverride: any = {}
-
-  if (server.parallel) {
-    Object.assign(configOverride, {
-      listen: {
-        port: server.port
-      },
-      webserver: {
-        port: server.port
-      },
-      database: {
-        suffix: '_test' + server.internalServerNumber
-      },
-      storage: {
-        tmp: `test${server.internalServerNumber}/tmp/`,
-        avatars: `test${server.internalServerNumber}/avatars/`,
-        videos: `test${server.internalServerNumber}/videos/`,
-        streaming_playlists: `test${server.internalServerNumber}/streaming-playlists/`,
-        redundancy: `test${server.internalServerNumber}/redundancy/`,
-        logs: `test${server.internalServerNumber}/logs/`,
-        previews: `test${server.internalServerNumber}/previews/`,
-        thumbnails: `test${server.internalServerNumber}/thumbnails/`,
-        torrents: `test${server.internalServerNumber}/torrents/`,
-        captions: `test${server.internalServerNumber}/captions/`,
-        cache: `test${server.internalServerNumber}/cache/`,
-        plugins: `test${server.internalServerNumber}/plugins/`
-      },
-      admin: {
-        email: `admin${server.internalServerNumber}@example.com`
-      },
-      live: {
-        rtmp: {
-          port: server.rtmpPort
-        }
-      }
-    })
-  }
-
-  if (configOverrideArg !== undefined) {
-    Object.assign(configOverride, configOverrideArg)
-  }
-
-  // Share the environment
-  const env = Object.create(process.env)
-  env['NODE_ENV'] = 'test'
-  env['NODE_APP_INSTANCE'] = server.internalServerNumber.toString()
-  env['NODE_CONFIG'] = JSON.stringify(configOverride)
-
-  const forkOptions = {
-    silent: true,
-    env,
-    detached: true,
-    execArgv: options.execArgv || []
-  }
-
-  return new Promise<ServerInfo>(res => {
-    server.app = fork(join(root(), 'dist', 'server.js'), args, forkOptions)
-    server.app.stdout.on('data', function onStdout (data) {
-      let dontContinue = false
-
-      // Capture things if we want to
-      for (const key of Object.keys(regexps)) {
-        const regexp = regexps[key]
-        const matches = data.toString().match(regexp)
-        if (matches !== null) {
-          if (key === 'client_id') server.client.id = matches[1]
-          else if (key === 'client_secret') server.client.secret = matches[1]
-          else if (key === 'user_username') server.user.username = matches[1]
-          else if (key === 'user_password') server.user.password = matches[1]
-        }
-      }
-
-      // Check if all required sentences are here
-      for (const key of Object.keys(serverRunString)) {
-        if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
-        if (serverRunString[key] === false) dontContinue = true
-      }
-
-      // If no, there is maybe one thing not already initialized (client/user credentials generation...)
-      if (dontContinue === true) return
-
-      if (options.hideLogs === false) {
-        console.log(data.toString())
-      } else {
-        server.app.stdout.removeListener('data', onStdout)
-      }
-
-      process.on('exit', () => {
-        try {
-          process.kill(server.app.pid)
-        } catch { /* empty */ }
-      })
-
-      res(server)
-    })
-  })
-}
-
-async function reRunServer (server: ServerInfo, configOverride?: any) {
-  const newServer = await runServer(server, configOverride)
-  server.app = newServer.app
+  await server.flushAndRun(configOverride, options)
 
   return server
 }
 
-async function checkTmpIsEmpty (server: ServerInfo) {
-  await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
+function createMultipleServers (totalServers: number, configOverride?: Object) {
+  const serverPromises: Promise<PeerTubeServer>[] = []
 
-  if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) {
-    await checkDirectoryIsEmpty(server, 'tmp/hls')
+  for (let i = 1; i <= totalServers; i++) {
+    serverPromises.push(createSingleServer(i, configOverride))
   }
-}
-
-async function checkDirectoryIsEmpty (server: ServerInfo, directory: string, exceptions: string[] = []) {
-  const testDirectory = 'test' + server.internalServerNumber
-
-  const directoryPath = join(root(), testDirectory, directory)
 
-  const directoryExists = await pathExists(directoryPath)
-  expect(directoryExists).to.be.true
-
-  const files = await readdir(directoryPath)
-  const filtered = files.filter(f => exceptions.includes(f) === false)
-
-  expect(filtered).to.have.lengthOf(0)
+  return Promise.all(serverPromises)
 }
 
-function killallServers (servers: ServerInfo[]) {
-  for (const server of servers) {
-    if (!server.app) continue
-
-    process.kill(-server.app.pid)
-    server.app = null
-  }
+async function killallServers (servers: PeerTubeServer[]) {
+  return Promise.all(servers.map(s => s.kill()))
 }
 
-async function cleanupTests (servers: ServerInfo[]) {
-  killallServers(servers)
+async function cleanupTests (servers: PeerTubeServer[]) {
+  await killallServers(servers)
 
   if (isGithubCI()) {
     await ensureDir('artifacts')
   }
 
-  const p: Promise<any>[] = []
+  let p: Promise<any>[] = []
   for (const server of servers) {
-    if (isGithubCI()) {
-      const origin = await buildServerDirectory(server, 'logs/peertube.log')
-      const destname = `peertube-${server.internalServerNumber}.log`
-      console.log('Saving logs %s.', destname)
-
-      await copy(origin, join('artifacts', destname))
-    }
-
-    if (server.parallel) {
-      p.push(flushTests(server.internalServerNumber))
-    }
-
-    if (server.customConfigFile) {
-      p.push(remove(server.customConfigFile))
-    }
+    p = p.concat(server.servers.cleanupTests())
   }
 
   return Promise.all(p)
 }
 
-async function waitUntilLog (server: ServerInfo, str: string, count = 1, strictCount = true) {
-  const logfile = buildServerDirectory(server, 'logs/peertube.log')
-
-  while (true) {
-    const buf = await readFile(logfile)
-
-    const matches = buf.toString().match(new RegExp(str, 'g'))
-    if (matches && matches.length === count) return
-    if (matches && strictCount === false && matches.length >= count) return
-
-    await wait(1000)
-  }
-}
-
-async function getServerFileSize (server: ServerInfo, subPath: string) {
-  const path = buildServerDirectory(server, subPath)
-
-  return getFileSize(path)
-}
-
-function makePingRequest (server: ServerInfo) {
-  return makeGetRequest({
-    url: server.url,
-    path: '/api/v1/ping',
-    statusCodeExpected: 200
-  })
-}
-
 // ---------------------------------------------------------------------------
 
 export {
-  checkDirectoryIsEmpty,
-  checkTmpIsEmpty,
-  getServerFileSize,
-  ServerInfo,
-  parallelTests,
+  createSingleServer,
+  createMultipleServers,
   cleanupTests,
-  flushAndRunMultipleServers,
-  flushTests,
-  makePingRequest,
-  flushAndRunServer,
-  killallServers,
-  reRunServer,
-  waitUntilLog
+  killallServers
 }
diff --git a/shared/extra-utils/server/stats-command.ts b/shared/extra-utils/server/stats-command.ts
new file mode 100644 (file)
index 0000000..64a4523
--- /dev/null
@@ -0,0 +1,25 @@
+import { HttpStatusCode, ServerStats } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class StatsCommand extends AbstractCommand {
+
+  get (options: OverrideCommandOptions & {
+    useCache?: boolean // default false
+  } = {}) {
+    const { useCache = false } = options
+    const path = '/api/v1/server/stats'
+
+    const query = {
+      t: useCache ? undefined : new Date().getTime()
+    }
+
+    return this.getRequestBody<ServerStats>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
diff --git a/shared/extra-utils/server/stats.ts b/shared/extra-utils/server/stats.ts
deleted file mode 100644 (file)
index b9dae24..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import { makeGetRequest } from '../requests/requests'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getStats (url: string, useCache = false) {
-  const path = '/api/v1/server/stats'
-
-  const query = {
-    t: useCache ? undefined : new Date().getTime()
-  }
-
-  return makeGetRequest({
-    url,
-    path,
-    query,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getStats
-}
diff --git a/shared/extra-utils/shared/abstract-command.ts b/shared/extra-utils/shared/abstract-command.ts
new file mode 100644 (file)
index 0000000..021045e
--- /dev/null
@@ -0,0 +1,199 @@
+import { isAbsolute, join } from 'path'
+import { root } from '../miscs/tests'
+import {
+  makeDeleteRequest,
+  makeGetRequest,
+  makePostBodyRequest,
+  makePutBodyRequest,
+  makeUploadRequest,
+  unwrapBody,
+  unwrapText
+} from '../requests/requests'
+import { PeerTubeServer } from '../server/server'
+
+export interface OverrideCommandOptions {
+  token?: string
+  expectedStatus?: number
+}
+
+interface InternalCommonCommandOptions extends OverrideCommandOptions {
+  // Default to server.url
+  url?: string
+
+  path: string
+  // If we automatically send the server token if the token is not provided
+  implicitToken: boolean
+  defaultExpectedStatus: number
+
+  // Common optional request parameters
+  contentType?: string
+  accept?: string
+  redirects?: number
+  range?: string
+  host?: string
+  headers?: { [ name: string ]: string }
+  requestType?: string
+  xForwardedFor?: string
+}
+
+interface InternalGetCommandOptions extends InternalCommonCommandOptions {
+  query?: { [ id: string ]: any }
+}
+
+abstract class AbstractCommand {
+
+  constructor (
+    protected server: PeerTubeServer
+  ) {
+
+  }
+
+  protected getRequestBody <T> (options: InternalGetCommandOptions) {
+    return unwrapBody<T>(this.getRequest(options))
+  }
+
+  protected getRequestText (options: InternalGetCommandOptions) {
+    return unwrapText(this.getRequest(options))
+  }
+
+  protected getRawRequest (options: Omit<InternalGetCommandOptions, 'path'>) {
+    const { url, range } = options
+    const { host, protocol, pathname } = new URL(url)
+
+    return this.getRequest({
+      ...options,
+
+      token: this.buildCommonRequestToken(options),
+      defaultExpectedStatus: this.buildExpectedStatus(options),
+
+      url: `${protocol}//${host}`,
+      path: pathname,
+      range
+    })
+  }
+
+  protected getRequest (options: InternalGetCommandOptions) {
+    const { query } = options
+
+    return makeGetRequest({
+      ...this.buildCommonRequestOptions(options),
+
+      query
+    })
+  }
+
+  protected deleteRequest (options: InternalCommonCommandOptions) {
+    return makeDeleteRequest(this.buildCommonRequestOptions(options))
+  }
+
+  protected putBodyRequest (options: InternalCommonCommandOptions & {
+    fields?: { [ fieldName: string ]: any }
+  }) {
+    const { fields } = options
+
+    return makePutBodyRequest({
+      ...this.buildCommonRequestOptions(options),
+
+      fields
+    })
+  }
+
+  protected postBodyRequest (options: InternalCommonCommandOptions & {
+    fields?: { [ fieldName: string ]: any }
+  }) {
+    const { fields } = options
+
+    return makePostBodyRequest({
+      ...this.buildCommonRequestOptions(options),
+
+      fields
+    })
+  }
+
+  protected postUploadRequest (options: InternalCommonCommandOptions & {
+    fields?: { [ fieldName: string ]: any }
+    attaches?: { [ fieldName: string ]: any }
+  }) {
+    const { fields, attaches } = options
+
+    return makeUploadRequest({
+      ...this.buildCommonRequestOptions(options),
+
+      method: 'POST',
+      fields,
+      attaches
+    })
+  }
+
+  protected putUploadRequest (options: InternalCommonCommandOptions & {
+    fields?: { [ fieldName: string ]: any }
+    attaches?: { [ fieldName: string ]: any }
+  }) {
+    const { fields, attaches } = options
+
+    return makeUploadRequest({
+      ...this.buildCommonRequestOptions(options),
+
+      method: 'PUT',
+      fields,
+      attaches
+    })
+  }
+
+  protected updateImageRequest (options: InternalCommonCommandOptions & {
+    fixture: string
+    fieldname: string
+  }) {
+    const filePath = isAbsolute(options.fixture)
+      ? options.fixture
+      : join(root(), 'server', 'tests', 'fixtures', options.fixture)
+
+    return this.postUploadRequest({
+      ...options,
+
+      fields: {},
+      attaches: { [options.fieldname]: filePath }
+    })
+  }
+
+  protected buildCommonRequestOptions (options: InternalCommonCommandOptions) {
+    const { url, path, redirects, contentType, accept, range, host, headers, requestType, xForwardedFor } = options
+
+    return {
+      url: url ?? this.server.url,
+      path,
+
+      token: this.buildCommonRequestToken(options),
+      expectedStatus: this.buildExpectedStatus(options),
+
+      redirects,
+      contentType,
+      range,
+      host,
+      accept,
+      headers,
+      type: requestType,
+      xForwardedFor
+    }
+  }
+
+  protected buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
+    const { token } = options
+
+    const fallbackToken = options.implicitToken
+      ? this.server.accessToken
+      : undefined
+
+    return token !== undefined ? token : fallbackToken
+  }
+
+  protected buildExpectedStatus (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
+    const { expectedStatus, defaultExpectedStatus } = options
+
+    return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus
+  }
+}
+
+export {
+  AbstractCommand
+}
diff --git a/shared/extra-utils/shared/index.ts b/shared/extra-utils/shared/index.ts
new file mode 100644 (file)
index 0000000..e807ab4
--- /dev/null
@@ -0,0 +1 @@
+export * from './abstract-command'
diff --git a/shared/extra-utils/socket/index.ts b/shared/extra-utils/socket/index.ts
new file mode 100644 (file)
index 0000000..594329b
--- /dev/null
@@ -0,0 +1 @@
+export * from './socket-io-command'
diff --git a/shared/extra-utils/socket/socket-io-command.ts b/shared/extra-utils/socket/socket-io-command.ts
new file mode 100644 (file)
index 0000000..c277ead
--- /dev/null
@@ -0,0 +1,15 @@
+import { io } from 'socket.io-client'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class SocketIOCommand extends AbstractCommand {
+
+  getUserNotificationSocket (options: OverrideCommandOptions = {}) {
+    return io(this.server.url + '/user-notifications', {
+      query: { accessToken: options.token ?? this.server.accessToken }
+    })
+  }
+
+  getLiveNotificationSocket () {
+    return io(this.server.url + '/live-videos')
+  }
+}
diff --git a/shared/extra-utils/socket/socket-io.ts b/shared/extra-utils/socket/socket-io.ts
deleted file mode 100644 (file)
index 4ca93f4..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-import { io } from 'socket.io-client'
-
-function getUserNotificationSocket (serverUrl: string, accessToken: string) {
-  return io(serverUrl + '/user-notifications', {
-    query: { accessToken }
-  })
-}
-
-function getLiveNotificationSocket (serverUrl: string) {
-  return io(serverUrl + '/live-videos')
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getUserNotificationSocket,
-  getLiveNotificationSocket
-}
diff --git a/shared/extra-utils/users/accounts-command.ts b/shared/extra-utils/users/accounts-command.ts
new file mode 100644 (file)
index 0000000..2f58610
--- /dev/null
@@ -0,0 +1,56 @@
+import { HttpStatusCode, ResultList } from '@shared/models'
+import { Account } from '../../models/actors'
+import { AccountVideoRate, VideoRateType } from '../../models/videos'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class AccountsCommand extends AbstractCommand {
+
+  list (options: OverrideCommandOptions & {
+    sort?: string // default -createdAt
+  } = {}) {
+    const { sort = '-createdAt' } = options
+    const path = '/api/v1/accounts'
+
+    return this.getRequestBody<ResultList<Account>>({
+      ...options,
+
+      path,
+      query: { sort },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  get (options: OverrideCommandOptions & {
+    accountName: string
+  }) {
+    const path = '/api/v1/accounts/' + options.accountName
+
+    return this.getRequestBody<Account>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listRatings (options: OverrideCommandOptions & {
+    accountName: string
+    rating?: VideoRateType
+  }) {
+    const { rating, accountName } = options
+    const path = '/api/v1/accounts/' + accountName + '/ratings'
+
+    const query = { rating }
+
+    return this.getRequestBody<ResultList<AccountVideoRate>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
diff --git a/shared/extra-utils/users/accounts.ts b/shared/extra-utils/users/accounts.ts
deleted file mode 100644 (file)
index 4ea7f14..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-
-import * as request from 'supertest'
-import { expect } from 'chai'
-import { existsSync, readdir } from 'fs-extra'
-import { join } from 'path'
-import { Account } from '../../models/actors'
-import { root } from '../miscs/miscs'
-import { makeGetRequest } from '../requests/requests'
-import { VideoRateType } from '../../models/videos'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getAccountsList (url: string, sort = '-createdAt', statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/accounts'
-
-  return makeGetRequest({
-    url,
-    query: { sort },
-    path,
-    statusCodeExpected
-  })
-}
-
-function getAccount (url: string, accountName: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/accounts/' + accountName
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected
-  })
-}
-
-async function expectAccountFollows (url: string, nameWithDomain: string, followersCount: number, followingCount: number) {
-  const res = await getAccountsList(url)
-  const account = res.body.data.find((a: Account) => a.name + '@' + a.host === nameWithDomain)
-
-  const message = `${nameWithDomain} on ${url}`
-  expect(account.followersCount).to.equal(followersCount, message)
-  expect(account.followingCount).to.equal(followingCount, message)
-}
-
-async function checkActorFilesWereRemoved (filename: string, serverNumber: number) {
-  const testDirectory = 'test' + serverNumber
-
-  for (const directory of [ 'avatars' ]) {
-    const directoryPath = join(root(), testDirectory, directory)
-
-    const directoryExists = existsSync(directoryPath)
-    expect(directoryExists).to.be.true
-
-    const files = await readdir(directoryPath)
-    for (const file of files) {
-      expect(file).to.not.contain(filename)
-    }
-  }
-}
-
-function getAccountRatings (
-  url: string,
-  accountName: string,
-  accessToken: string,
-  rating?: VideoRateType,
-  statusCodeExpected = HttpStatusCode.OK_200
-) {
-  const path = '/api/v1/accounts/' + accountName + '/ratings'
-
-  const query = rating ? { rating } : {}
-
-  return request(url)
-          .get(path)
-          .query(query)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + accessToken)
-          .expect(statusCodeExpected)
-          .expect('Content-Type', /json/)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getAccount,
-  expectAccountFollows,
-  getAccountsList,
-  checkActorFilesWereRemoved,
-  getAccountRatings
-}
diff --git a/shared/extra-utils/users/actors.ts b/shared/extra-utils/users/actors.ts
new file mode 100644 (file)
index 0000000..cfcc7d0
--- /dev/null
@@ -0,0 +1,73 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { expect } from 'chai'
+import { pathExists, readdir } from 'fs-extra'
+import { join } from 'path'
+import { root } from '@server/helpers/core-utils'
+import { Account, VideoChannel } from '@shared/models'
+import { PeerTubeServer } from '../server'
+
+async function expectChannelsFollows (options: {
+  server: PeerTubeServer
+  handle: string
+  followers: number
+  following: number
+}) {
+  const { server } = options
+  const { data } = await server.channels.list()
+
+  return expectActorFollow({ ...options, data })
+}
+
+async function expectAccountFollows (options: {
+  server: PeerTubeServer
+  handle: string
+  followers: number
+  following: number
+}) {
+  const { server } = options
+  const { data } = await server.accounts.list()
+
+  return expectActorFollow({ ...options, data })
+}
+
+async function checkActorFilesWereRemoved (filename: string, serverNumber: number) {
+  const testDirectory = 'test' + serverNumber
+
+  for (const directory of [ 'avatars' ]) {
+    const directoryPath = join(root(), testDirectory, directory)
+
+    const directoryExists = await pathExists(directoryPath)
+    expect(directoryExists).to.be.true
+
+    const files = await readdir(directoryPath)
+    for (const file of files) {
+      expect(file).to.not.contain(filename)
+    }
+  }
+}
+
+export {
+  expectAccountFollows,
+  expectChannelsFollows,
+  checkActorFilesWereRemoved
+}
+
+// ---------------------------------------------------------------------------
+
+function expectActorFollow (options: {
+  server: PeerTubeServer
+  data: (Account | VideoChannel)[]
+  handle: string
+  followers: number
+  following: number
+}) {
+  const { server, data, handle, followers, following } = options
+
+  const actor = data.find(a => a.name + '@' + a.host === handle)
+  const message = `${handle} on ${server.url}`
+
+  expect(actor, message).to.exist
+  expect(actor.followersCount).to.equal(followers, message)
+  expect(actor.followingCount).to.equal(following, message)
+}
diff --git a/shared/extra-utils/users/blocklist-command.ts b/shared/extra-utils/users/blocklist-command.ts
new file mode 100644 (file)
index 0000000..14491a1
--- /dev/null
@@ -0,0 +1,139 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { AccountBlock, HttpStatusCode, ResultList, ServerBlock } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+type ListBlocklistOptions = OverrideCommandOptions & {
+  start: number
+  count: number
+  sort: string // default -createdAt
+}
+
+export class BlocklistCommand extends AbstractCommand {
+
+  listMyAccountBlocklist (options: ListBlocklistOptions) {
+    const path = '/api/v1/users/me/blocklist/accounts'
+
+    return this.listBlocklist<AccountBlock>(options, path)
+  }
+
+  listMyServerBlocklist (options: ListBlocklistOptions) {
+    const path = '/api/v1/users/me/blocklist/servers'
+
+    return this.listBlocklist<ServerBlock>(options, path)
+  }
+
+  listServerAccountBlocklist (options: ListBlocklistOptions) {
+    const path = '/api/v1/server/blocklist/accounts'
+
+    return this.listBlocklist<AccountBlock>(options, path)
+  }
+
+  listServerServerBlocklist (options: ListBlocklistOptions) {
+    const path = '/api/v1/server/blocklist/servers'
+
+    return this.listBlocklist<ServerBlock>(options, path)
+  }
+
+  // ---------------------------------------------------------------------------
+
+  addToMyBlocklist (options: OverrideCommandOptions & {
+    account?: string
+    server?: string
+  }) {
+    const { account, server } = options
+
+    const path = account
+      ? '/api/v1/users/me/blocklist/accounts'
+      : '/api/v1/users/me/blocklist/servers'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: {
+        accountName: account,
+        host: server
+      },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  addToServerBlocklist (options: OverrideCommandOptions & {
+    account?: string
+    server?: string
+  }) {
+    const { account, server } = options
+
+    const path = account
+      ? '/api/v1/server/blocklist/accounts'
+      : '/api/v1/server/blocklist/servers'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: {
+        accountName: account,
+        host: server
+      },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  removeFromMyBlocklist (options: OverrideCommandOptions & {
+    account?: string
+    server?: string
+  }) {
+    const { account, server } = options
+
+    const path = account
+      ? '/api/v1/users/me/blocklist/accounts/' + account
+      : '/api/v1/users/me/blocklist/servers/' + server
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  removeFromServerBlocklist (options: OverrideCommandOptions & {
+    account?: string
+    server?: string
+  }) {
+    const { account, server } = options
+
+    const path = account
+      ? '/api/v1/server/blocklist/accounts/' + account
+      : '/api/v1/server/blocklist/servers/' + server
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  private listBlocklist <T> (options: ListBlocklistOptions, path: string) {
+    const { start, count, sort = '-createdAt' } = options
+
+    return this.getRequestBody<ResultList<T>>({
+      ...options,
+
+      path,
+      query: { start, count, sort },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+}
diff --git a/shared/extra-utils/users/blocklist.ts b/shared/extra-utils/users/blocklist.ts
deleted file mode 100644 (file)
index bdf7ee5..0000000
+++ /dev/null
@@ -1,238 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-
-import { makeGetRequest, makeDeleteRequest, makePostBodyRequest } from '../requests/requests'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getAccountBlocklistByAccount (
-  url: string,
-  token: string,
-  start: number,
-  count: number,
-  sort = '-createdAt',
-  statusCodeExpected = HttpStatusCode.OK_200
-) {
-  const path = '/api/v1/users/me/blocklist/accounts'
-
-  return makeGetRequest({
-    url,
-    token,
-    query: { start, count, sort },
-    path,
-    statusCodeExpected
-  })
-}
-
-function addAccountToAccountBlocklist (
-  url: string,
-  token: string,
-  accountToBlock: string,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/users/me/blocklist/accounts'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    fields: {
-      accountName: accountToBlock
-    },
-    statusCodeExpected
-  })
-}
-
-function removeAccountFromAccountBlocklist (
-  url: string,
-  token: string,
-  accountToUnblock: string,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock
-
-  return makeDeleteRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-function getServerBlocklistByAccount (
-  url: string,
-  token: string,
-  start: number,
-  count: number,
-  sort = '-createdAt',
-  statusCodeExpected = HttpStatusCode.OK_200
-) {
-  const path = '/api/v1/users/me/blocklist/servers'
-
-  return makeGetRequest({
-    url,
-    token,
-    query: { start, count, sort },
-    path,
-    statusCodeExpected
-  })
-}
-
-function addServerToAccountBlocklist (
-  url: string,
-  token: string,
-  serverToBlock: string,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/users/me/blocklist/servers'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    fields: {
-      host: serverToBlock
-    },
-    statusCodeExpected
-  })
-}
-
-function removeServerFromAccountBlocklist (
-  url: string,
-  token: string,
-  serverToBlock: string,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock
-
-  return makeDeleteRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-function getAccountBlocklistByServer (
-  url: string,
-  token: string,
-  start: number,
-  count: number,
-  sort = '-createdAt',
-  statusCodeExpected = HttpStatusCode.OK_200
-) {
-  const path = '/api/v1/server/blocklist/accounts'
-
-  return makeGetRequest({
-    url,
-    token,
-    query: { start, count, sort },
-    path,
-    statusCodeExpected
-  })
-}
-
-function addAccountToServerBlocklist (
-  url: string,
-  token: string,
-  accountToBlock: string,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/server/blocklist/accounts'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    fields: {
-      accountName: accountToBlock
-    },
-    statusCodeExpected
-  })
-}
-
-function removeAccountFromServerBlocklist (
-  url: string,
-  token: string,
-  accountToUnblock: string,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/server/blocklist/accounts/' + accountToUnblock
-
-  return makeDeleteRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-function getServerBlocklistByServer (
-  url: string,
-  token: string,
-  start: number,
-  count: number,
-  sort = '-createdAt',
-  statusCodeExpected = HttpStatusCode.OK_200
-) {
-  const path = '/api/v1/server/blocklist/servers'
-
-  return makeGetRequest({
-    url,
-    token,
-    query: { start, count, sort },
-    path,
-    statusCodeExpected
-  })
-}
-
-function addServerToServerBlocklist (
-  url: string,
-  token: string,
-  serverToBlock: string,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/server/blocklist/servers'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    fields: {
-      host: serverToBlock
-    },
-    statusCodeExpected
-  })
-}
-
-function removeServerFromServerBlocklist (
-  url: string,
-  token: string,
-  serverToBlock: string,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/server/blocklist/servers/' + serverToBlock
-
-  return makeDeleteRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getAccountBlocklistByAccount,
-  addAccountToAccountBlocklist,
-  removeAccountFromAccountBlocklist,
-  getServerBlocklistByAccount,
-  addServerToAccountBlocklist,
-  removeServerFromAccountBlocklist,
-
-  getAccountBlocklistByServer,
-  addAccountToServerBlocklist,
-  removeAccountFromServerBlocklist,
-  getServerBlocklistByServer,
-  addServerToServerBlocklist,
-  removeServerFromServerBlocklist
-}
diff --git a/shared/extra-utils/users/index.ts b/shared/extra-utils/users/index.ts
new file mode 100644 (file)
index 0000000..460a06f
--- /dev/null
@@ -0,0 +1,9 @@
+export * from './accounts-command'
+export * from './actors'
+export * from './blocklist-command'
+export * from './login'
+export * from './login-command'
+export * from './notifications'
+export * from './notifications-command'
+export * from './subscriptions-command'
+export * from './users-command'
diff --git a/shared/extra-utils/users/login-command.ts b/shared/extra-utils/users/login-command.ts
new file mode 100644 (file)
index 0000000..143f72a
--- /dev/null
@@ -0,0 +1,132 @@
+import { HttpStatusCode, PeerTubeProblemDocument } from '@shared/models'
+import { unwrapBody } from '../requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class LoginCommand extends AbstractCommand {
+
+  login (options: OverrideCommandOptions & {
+    client?: { id?: string, secret?: string }
+    user?: { username: string, password?: string }
+  } = {}) {
+    const { client = this.server.store.client, user = this.server.store.user } = options
+    const path = '/api/v1/users/token'
+
+    const body = {
+      client_id: client.id,
+      client_secret: client.secret,
+      username: user.username,
+      password: user.password ?? 'password',
+      response_type: 'code',
+      grant_type: 'password',
+      scope: 'upload'
+    }
+
+    return unwrapBody<{ access_token: string, refresh_token: string } & PeerTubeProblemDocument>(this.postBodyRequest({
+      ...options,
+
+      path,
+      requestType: 'form',
+      fields: body,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  getAccessToken (arg1?: { username: string, password?: string }): Promise<string>
+  getAccessToken (arg1: string, password?: string): Promise<string>
+  async getAccessToken (arg1?: { username: string, password?: string } | string, password?: string) {
+    let user: { username: string, password?: string }
+
+    if (!arg1) user = this.server.store.user
+    else if (typeof arg1 === 'object') user = arg1
+    else user = { username: arg1, password }
+
+    try {
+      const body = await this.login({ user })
+
+      return body.access_token
+    } catch (err) {
+      throw new Error(`Cannot authenticate. Please check your username/password. (${err})`)
+    }
+  }
+
+  loginUsingExternalToken (options: OverrideCommandOptions & {
+    username: string
+    externalAuthToken: string
+  }) {
+    const { username, externalAuthToken } = options
+    const path = '/api/v1/users/token'
+
+    const body = {
+      client_id: this.server.store.client.id,
+      client_secret: this.server.store.client.secret,
+      username: username,
+      response_type: 'code',
+      grant_type: 'password',
+      scope: 'upload',
+      externalAuthToken
+    }
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      requestType: 'form',
+      fields: body,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  logout (options: OverrideCommandOptions & {
+    token: string
+  }) {
+    const path = '/api/v1/users/revoke-token'
+
+    return unwrapBody<{ redirectUrl: string }>(this.postBodyRequest({
+      ...options,
+
+      path,
+      requestType: 'form',
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  refreshToken (options: OverrideCommandOptions & {
+    refreshToken: string
+  }) {
+    const path = '/api/v1/users/token'
+
+    const body = {
+      client_id: this.server.store.client.id,
+      client_secret: this.server.store.client.secret,
+      refresh_token: options.refreshToken,
+      response_type: 'code',
+      grant_type: 'refresh_token'
+    }
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      requestType: 'form',
+      fields: body,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getClient (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/oauth-clients/local'
+
+    return this.getRequestBody<{ client_id: string, client_secret: string }>({
+      ...options,
+
+      path,
+      host: this.server.host,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
index 39e1a2747d0e93ccd39ab549e9b58885b8928bbc..f1df027d3263d4c1bea10f902a387698f4703e15 100644 (file)
-import * as request from 'supertest'
+import { PeerTubeServer } from '../server/server'
 
-import { ServerInfo } from '../server/servers'
-import { getClient } from '../server/clients'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-type Client = { id: string, secret: string }
-type User = { username: string, password: string }
-type Server = { url: string, client: Client, user: User }
-
-function login (url: string, client: Client, user: User, expectedStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/token'
-
-  const body = {
-    client_id: client.id,
-    client_secret: client.secret,
-    username: user.username,
-    password: user.password,
-    response_type: 'code',
-    grant_type: 'password',
-    scope: 'upload'
-  }
-
-  return request(url)
-          .post(path)
-          .type('form')
-          .send(body)
-          .expect(expectedStatus)
-}
-
-function logout (url: string, token: string, expectedStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/revoke-token'
-
-  return request(url)
-    .post(path)
-    .set('Authorization', 'Bearer ' + token)
-    .type('form')
-    .expect(expectedStatus)
-}
-
-async function serverLogin (server: Server) {
-  const res = await login(server.url, server.client, server.user, HttpStatusCode.OK_200)
-
-  return res.body.access_token as string
-}
-
-function refreshToken (server: ServerInfo, refreshToken: string, expectedStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/token'
-
-  const body = {
-    client_id: server.client.id,
-    client_secret: server.client.secret,
-    refresh_token: refreshToken,
-    response_type: 'code',
-    grant_type: 'refresh_token'
-  }
-
-  return request(server.url)
-    .post(path)
-    .type('form')
-    .send(body)
-    .expect(expectedStatus)
-}
-
-async function userLogin (server: Server, user: User, expectedStatus = HttpStatusCode.OK_200) {
-  const res = await login(server.url, server.client, user, expectedStatus)
-
-  return res.body.access_token as string
-}
-
-async function getAccessToken (url: string, username: string, password: string) {
-  const resClient = await getClient(url)
-  const client = {
-    id: resClient.body.client_id,
-    secret: resClient.body.client_secret
-  }
-
-  const user = { username, password }
-
-  try {
-    const res = await login(url, client, user)
-    return res.body.access_token
-  } catch (err) {
-    throw new Error('Cannot authenticate. Please check your username/password.')
-  }
-}
-
-function setAccessTokensToServers (servers: ServerInfo[]) {
+function setAccessTokensToServers (servers: PeerTubeServer[]) {
   const tasks: Promise<any>[] = []
 
   for (const server of servers) {
-    const p = serverLogin(server).then(t => { server.accessToken = t })
+    const p = server.login.getAccessToken()
+                                 .then(t => { server.accessToken = t })
     tasks.push(p)
   }
 
   return Promise.all(tasks)
 }
 
-function loginUsingExternalToken (server: Server, username: string, externalAuthToken: string, expectedStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/token'
-
-  const body = {
-    client_id: server.client.id,
-    client_secret: server.client.secret,
-    username: username,
-    response_type: 'code',
-    grant_type: 'password',
-    scope: 'upload',
-    externalAuthToken
-  }
-
-  return request(server.url)
-          .post(path)
-          .type('form')
-          .send(body)
-          .expect(expectedStatus)
-}
-
 // ---------------------------------------------------------------------------
 
 export {
-  login,
-  logout,
-  serverLogin,
-  refreshToken,
-  userLogin,
-  getAccessToken,
-  setAccessTokensToServers,
-  Server,
-  Client,
-  User,
-  loginUsingExternalToken
+  setAccessTokensToServers
 }
diff --git a/shared/extra-utils/users/notifications-command.ts b/shared/extra-utils/users/notifications-command.ts
new file mode 100644 (file)
index 0000000..2d79a37
--- /dev/null
@@ -0,0 +1,86 @@
+import { HttpStatusCode, ResultList } from '@shared/models'
+import { UserNotification, UserNotificationSetting } from '../../models/users'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class NotificationsCommand extends AbstractCommand {
+
+  updateMySettings (options: OverrideCommandOptions & {
+    settings: UserNotificationSetting
+  }) {
+    const path = '/api/v1/users/me/notification-settings'
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: options.settings,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  list (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    unread?: boolean
+    sort?: string
+  }) {
+    const { start, count, unread, sort = '-createdAt' } = options
+    const path = '/api/v1/users/me/notifications'
+
+    return this.getRequestBody<ResultList<UserNotification>>({
+      ...options,
+
+      path,
+      query: {
+        start,
+        count,
+        sort,
+        unread
+      },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  markAsRead (options: OverrideCommandOptions & {
+    ids: number[]
+  }) {
+    const { ids } = options
+    const path = '/api/v1/users/me/notifications/read'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { ids },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  markAsReadAll (options: OverrideCommandOptions) {
+    const path = '/api/v1/users/me/notifications/read-all'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  async getLastest (options: OverrideCommandOptions = {}) {
+    const { total, data } = await this.list({
+      ...options,
+      start: 0,
+      count: 1,
+      sort: '-createdAt'
+    })
+
+    if (total === 0) return undefined
+
+    return data[0]
+  }
+}
similarity index 63%
rename from shared/extra-utils/users/user-notifications.ts
rename to shared/extra-utils/users/notifications.ts
index 844f4442dafbb81ae4765fed78642dbbbe16e541..7db4bfd3fc7109db9611c314b780d2aeb0c5dfa5 100644 (file)
@@ -3,91 +3,15 @@
 import { expect } from 'chai'
 import { inspect } from 'util'
 import { AbuseState, PluginType } from '@shared/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
-import { MockSmtpServer } from '../miscs/email'
-import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
+import { MockSmtpServer } from '../mock-servers/mock-email'
+import { PeerTubeServer } from '../server'
 import { doubleFollow } from '../server/follows'
-import { flushAndRunMultipleServers, ServerInfo } from '../server/servers'
-import { getUserNotificationSocket } from '../socket/socket-io'
-import { setAccessTokensToServers, userLogin } from './login'
-import { createUser, getMyUserInformation } from './users'
-
-function updateMyNotificationSettings (
-  url: string,
-  token: string,
-  settings: UserNotificationSetting,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/users/me/notification-settings'
-
-  return makePutBodyRequest({
-    url,
-    path,
-    token,
-    fields: settings,
-    statusCodeExpected
-  })
-}
-
-async function getUserNotifications (
-  url: string,
-  token: string,
-  start: number,
-  count: number,
-  unread?: boolean,
-  sort = '-createdAt',
-  statusCodeExpected = HttpStatusCode.OK_200
-) {
-  const path = '/api/v1/users/me/notifications'
-
-  return makeGetRequest({
-    url,
-    path,
-    token,
-    query: {
-      start,
-      count,
-      sort,
-      unread
-    },
-    statusCodeExpected
-  })
-}
-
-function markAsReadNotifications (url: string, token: string, ids: number[], statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/users/me/notifications/read'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    fields: { ids },
-    statusCodeExpected
-  })
-}
-
-function markAsReadAllNotifications (url: string, token: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/users/me/notifications/read-all'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-async function getLastNotification (serverUrl: string, accessToken: string) {
-  const res = await getUserNotifications(serverUrl, accessToken, 0, 1, undefined, '-createdAt')
-
-  if (res.body.total === 0) return undefined
-
-  return res.body.data[0] as UserNotification
-}
+import { createMultipleServers } from '../server/servers'
+import { setAccessTokensToServers } from './login'
 
 type CheckerBaseParams = {
-  server: ServerInfo
+  server: PeerTubeServer
   emails: any[]
   socketNotifications: UserNotification[]
   token: string
@@ -96,91 +20,41 @@ type CheckerBaseParams = {
 
 type CheckerType = 'presence' | 'absence'
 
-async function checkNotification (
-  base: CheckerBaseParams,
-  notificationChecker: (notification: UserNotification, type: CheckerType) => void,
-  emailNotificationFinder: (email: object) => boolean,
-  checkType: CheckerType
-) {
-  const check = base.check || { web: true, mail: true }
-
-  if (check.web) {
-    const notification = await getLastNotification(base.server.url, base.token)
-
-    if (notification || checkType !== 'absence') {
-      notificationChecker(notification, checkType)
-    }
-
-    const socketNotification = base.socketNotifications.find(n => {
-      try {
-        notificationChecker(n, 'presence')
-        return true
-      } catch {
-        return false
-      }
-    })
-
-    if (checkType === 'presence') {
-      const obj = inspect(base.socketNotifications, { depth: 5 })
-      expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined
-    } else {
-      const obj = inspect(socketNotification, { depth: 5 })
-      expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined
-    }
-  }
-
-  if (check.mail) {
-    // Last email
-    const email = base.emails
-                      .slice()
-                      .reverse()
-                      .find(e => emailNotificationFinder(e))
-
-    if (checkType === 'presence') {
-      const emails = base.emails.map(e => e.text)
-      expect(email, 'The email is absent when is should be present. ' + inspect(emails)).to.not.be.undefined
-    } else {
-      expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
-    }
-  }
-}
-
-function checkVideo (video: any, videoName?: string, videoUUID?: string) {
-  if (videoName) {
-    expect(video.name).to.be.a('string')
-    expect(video.name).to.not.be.empty
-    expect(video.name).to.equal(videoName)
-  }
-
-  if (videoUUID) {
-    expect(video.uuid).to.be.a('string')
-    expect(video.uuid).to.not.be.empty
-    expect(video.uuid).to.equal(videoUUID)
+function getAllNotificationsSettings (): UserNotificationSetting {
+  return {
+    newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
   }
-
-  expect(video.id).to.be.a('number')
 }
 
-function checkActor (actor: any) {
-  expect(actor.displayName).to.be.a('string')
-  expect(actor.displayName).to.not.be.empty
-  expect(actor.host).to.not.be.undefined
-}
-
-function checkComment (comment: any, commentId: number, threadId: number) {
-  expect(comment.id).to.equal(commentId)
-  expect(comment.threadId).to.equal(threadId)
-}
-
-async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
+async function checkNewVideoFromSubscription (options: CheckerBaseParams & {
+  videoName: string
+  shortUUID: string
+  checkType: CheckerType
+}) {
+  const { videoName, shortUUID } = options
   const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
-      checkVideo(notification.video, videoName, videoUUID)
+      checkVideo(notification.video, videoName, shortUUID)
       checkActor(notification.video.channel)
     } else {
       expect(notification).to.satisfy((n: UserNotification) => {
@@ -191,21 +65,26 @@ async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName
 
   function emailNotificationFinder (email: object) {
     const text = email['text']
-    return text.indexOf(videoUUID) !== -1 && text.indexOf('Your subscription') !== -1
+    return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
+async function checkVideoIsPublished (options: CheckerBaseParams & {
+  videoName: string
+  shortUUID: string
+  checkType: CheckerType
+}) {
+  const { videoName, shortUUID } = options
   const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
-      checkVideo(notification.video, videoName, videoUUID)
+      checkVideo(notification.video, videoName, shortUUID)
       checkActor(notification.video.channel)
     } else {
       expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
@@ -214,30 +93,31 @@ async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string
 
   function emailNotificationFinder (email: object) {
     const text: string = email['text']
-    return text.includes(videoUUID) && text.includes('Your video')
+    return text.includes(shortUUID) && text.includes('Your video')
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkMyVideoImportIsFinished (
-  base: CheckerBaseParams,
-  videoName: string,
-  videoUUID: string,
-  url: string,
-  success: boolean,
-  type: CheckerType
-) {
+async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
+  videoName: string
+  shortUUID: string
+  url: string
+  success: boolean
+  checkType: CheckerType
+}) {
+  const { videoName, shortUUID, url, success } = options
+
   const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
       expect(notification.videoImport.targetUrl).to.equal(url)
 
-      if (success) checkVideo(notification.videoImport.video, videoName, videoUUID)
+      if (success) checkVideo(notification.videoImport.video, videoName, shortUUID)
     } else {
       expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
     }
@@ -250,14 +130,18 @@ async function checkMyVideoImportIsFinished (
     return text.includes(url) && text.includes(toFind)
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkUserRegistered (base: CheckerBaseParams, username: string, type: CheckerType) {
+async function checkUserRegistered (options: CheckerBaseParams & {
+  username: string
+  checkType: CheckerType
+}) {
+  const { username } = options
   const notificationType = UserNotificationType.NEW_USER_REGISTRATION
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
@@ -274,21 +158,21 @@ async function checkUserRegistered (base: CheckerBaseParams, username: string, t
     return text.includes(' registered.') && text.includes(username)
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkNewActorFollow (
-  base: CheckerBaseParams,
-  followType: 'channel' | 'account',
-  followerName: string,
-  followerDisplayName: string,
-  followingDisplayName: string,
-  type: CheckerType
-) {
+async function checkNewActorFollow (options: CheckerBaseParams & {
+  followType: 'channel' | 'account'
+  followerName: string
+  followerDisplayName: string
+  followingDisplayName: string
+  checkType: CheckerType
+}) {
+  const { followType, followerName, followerDisplayName, followingDisplayName } = options
   const notificationType = UserNotificationType.NEW_FOLLOW
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
@@ -314,14 +198,18 @@ async function checkNewActorFollow (
     return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost: string, type: CheckerType) {
+async function checkNewInstanceFollower (options: CheckerBaseParams & {
+  followerHost: string
+  checkType: CheckerType
+}) {
+  const { followerHost } = options
   const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
@@ -343,14 +231,19 @@ async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost:
     return text.includes('instance has a new follower') && text.includes(followerHost)
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost: string, followingHost: string, type: CheckerType) {
+async function checkAutoInstanceFollowing (options: CheckerBaseParams & {
+  followerHost: string
+  followingHost: string
+  checkType: CheckerType
+}) {
+  const { followerHost, followingHost } = options
   const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
@@ -374,21 +267,21 @@ async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost
     return text.includes(' automatically followed a new instance') && text.includes(followingHost)
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkCommentMention (
-  base: CheckerBaseParams,
-  uuid: string,
-  commentId: number,
-  threadId: number,
-  byAccountDisplayName: string,
-  type: CheckerType
-) {
+async function checkCommentMention (options: CheckerBaseParams & {
+  shortUUID: string
+  commentId: number
+  threadId: number
+  byAccountDisplayName: string
+  checkType: CheckerType
+}) {
+  const { shortUUID, commentId, threadId, byAccountDisplayName } = options
   const notificationType = UserNotificationType.COMMENT_MENTION
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
@@ -396,7 +289,7 @@ async function checkCommentMention (
       checkActor(notification.comment.account)
       expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
 
-      checkVideo(notification.comment.video, undefined, uuid)
+      checkVideo(notification.comment.video, undefined, shortUUID)
     } else {
       expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
     }
@@ -405,25 +298,31 @@ async function checkCommentMention (
   function emailNotificationFinder (email: object) {
     const text: string = email['text']
 
-    return text.includes(' mentioned ') && text.includes(uuid) && text.includes(byAccountDisplayName)
+    return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName)
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
 let lastEmailCount = 0
 
-async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) {
+async function checkNewCommentOnMyVideo (options: CheckerBaseParams & {
+  shortUUID: string
+  commentId: number
+  threadId: number
+  checkType: CheckerType
+}) {
+  const { server, shortUUID, commentId, threadId, checkType, emails } = options
   const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
       checkComment(notification.comment, commentId, threadId)
       checkActor(notification.comment.account)
-      checkVideo(notification.comment.video, undefined, uuid)
+      checkVideo(notification.comment.video, undefined, shortUUID)
     } else {
       expect(notification).to.satisfy((n: UserNotification) => {
         return n === undefined || n.comment === undefined || n.comment.id !== commentId
@@ -431,51 +330,62 @@ async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string,
     }
   }
 
-  const commentUrl = `http://localhost:${base.server.port}/w/${uuid};threadId=${threadId}`
+  const commentUrl = `http://localhost:${server.port}/w/${shortUUID};threadId=${threadId}`
 
   function emailNotificationFinder (email: object) {
     return email['text'].indexOf(commentUrl) !== -1
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 
-  if (type === 'presence') {
+  if (checkType === 'presence') {
     // We cannot detect email duplicates, so check we received another email
-    expect(base.emails).to.have.length.above(lastEmailCount)
-    lastEmailCount = base.emails.length
+    expect(emails).to.have.length.above(lastEmailCount)
+    lastEmailCount = emails.length
   }
 }
 
-async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
+async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & {
+  shortUUID: string
+  videoName: string
+  checkType: CheckerType
+}) {
+  const { shortUUID, videoName } = options
   const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
       expect(notification.abuse.id).to.be.a('number')
-      checkVideo(notification.abuse.video, videoName, videoUUID)
+      checkVideo(notification.abuse.video, videoName, shortUUID)
     } else {
       expect(notification).to.satisfy((n: UserNotification) => {
-        return n === undefined || n.abuse === undefined || n.abuse.video.uuid !== videoUUID
+        return n === undefined || n.abuse === undefined || n.abuse.video.shortUUID !== shortUUID
       })
     }
   }
 
   function emailNotificationFinder (email: object) {
     const text = email['text']
-    return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1
+    return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkNewAbuseMessage (base: CheckerBaseParams, abuseId: number, message: string, toEmail: string, type: CheckerType) {
+async function checkNewAbuseMessage (options: CheckerBaseParams & {
+  abuseId: number
+  message: string
+  toEmail: string
+  checkType: CheckerType
+}) {
+  const { abuseId, message, toEmail } = options
   const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
@@ -494,14 +404,19 @@ async function checkNewAbuseMessage (base: CheckerBaseParams, abuseId: number, m
     return text.indexOf(message) !== -1 && to.length !== 0
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkAbuseStateChange (base: CheckerBaseParams, abuseId: number, state: AbuseState, type: CheckerType) {
+async function checkAbuseStateChange (options: CheckerBaseParams & {
+  abuseId: number
+  state: AbuseState
+  checkType: CheckerType
+}) {
+  const { abuseId, state } = options
   const notificationType = UserNotificationType.ABUSE_STATE_CHANGE
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
@@ -524,39 +439,48 @@ async function checkAbuseStateChange (base: CheckerBaseParams, abuseId: number,
     return text.indexOf(contains) !== -1
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkNewCommentAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
+async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & {
+  shortUUID: string
+  videoName: string
+  checkType: CheckerType
+}) {
+  const { shortUUID, videoName } = options
   const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
       expect(notification.abuse.id).to.be.a('number')
-      checkVideo(notification.abuse.comment.video, videoName, videoUUID)
+      checkVideo(notification.abuse.comment.video, videoName, shortUUID)
     } else {
       expect(notification).to.satisfy((n: UserNotification) => {
-        return n === undefined || n.abuse === undefined || n.abuse.comment.video.uuid !== videoUUID
+        return n === undefined || n.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID
       })
     }
   }
 
   function emailNotificationFinder (email: object) {
     const text = email['text']
-    return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1
+    return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkNewAccountAbuseForModerators (base: CheckerBaseParams, displayName: string, type: CheckerType) {
+async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & {
+  displayName: string
+  checkType: CheckerType
+}) {
+  const { displayName } = options
   const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
@@ -574,40 +498,45 @@ async function checkNewAccountAbuseForModerators (base: CheckerBaseParams, displ
     return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
+async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & {
+  shortUUID: string
+  videoName: string
+  checkType: CheckerType
+}) {
+  const { shortUUID, videoName } = options
   const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
       expect(notification.videoBlacklist.video.id).to.be.a('number')
-      checkVideo(notification.videoBlacklist.video, videoName, videoUUID)
+      checkVideo(notification.videoBlacklist.video, videoName, shortUUID)
     } else {
       expect(notification).to.satisfy((n: UserNotification) => {
-        return n === undefined || n.video === undefined || n.video.uuid !== videoUUID
+        return n === undefined || n.video === undefined || n.video.shortUUID !== shortUUID
       })
     }
   }
 
   function emailNotificationFinder (email: object) {
     const text = email['text']
-    return text.indexOf(videoUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1
+    return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkNewBlacklistOnMyVideo (
-  base: CheckerBaseParams,
-  videoUUID: string,
-  videoName: string,
+async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & {
+  shortUUID: string
+  videoName: string
   blacklistType: 'blacklist' | 'unblacklist'
-) {
+}) {
+  const { videoName, shortUUID, blacklistType } = options
   const notificationType = blacklistType === 'blacklist'
     ? UserNotificationType.BLACKLIST_ON_MY_VIDEO
     : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
@@ -618,22 +547,30 @@ async function checkNewBlacklistOnMyVideo (
 
     const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
 
-    checkVideo(video, videoName, videoUUID)
+    checkVideo(video, videoName, shortUUID)
   }
 
   function emailNotificationFinder (email: object) {
     const text = email['text']
-    return text.indexOf(videoUUID) !== -1 && text.indexOf(' ' + blacklistType) !== -1
+    const blacklistText = blacklistType === 'blacklist'
+      ? 'blacklisted'
+      : 'unblacklisted'
+
+    return text.includes(shortUUID) && text.includes(blacklistText)
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence')
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' })
 }
 
-async function checkNewPeerTubeVersion (base: CheckerBaseParams, latestVersion: string, type: CheckerType) {
+async function checkNewPeerTubeVersion (options: CheckerBaseParams & {
+  latestVersion: string
+  checkType: CheckerType
+}) {
+  const { latestVersion } = options
   const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
@@ -652,14 +589,19 @@ async function checkNewPeerTubeVersion (base: CheckerBaseParams, latestVersion:
     return text.includes(latestVersion)
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
-async function checkNewPluginVersion (base: CheckerBaseParams, pluginType: PluginType, pluginName: string, type: CheckerType) {
+async function checkNewPluginVersion (options: CheckerBaseParams & {
+  pluginType: PluginType
+  pluginName: string
+  checkType: CheckerType
+}) {
+  const { pluginName, pluginType } = options
   const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
 
-  function notificationChecker (notification: UserNotification, type: CheckerType) {
-    if (type === 'presence') {
+  function notificationChecker (notification: UserNotification, checkType: CheckerType) {
+    if (checkType === 'presence') {
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
@@ -678,28 +620,7 @@ async function checkNewPluginVersion (base: CheckerBaseParams, pluginType: Plugi
     return text.includes(pluginName)
   }
 
-  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
-}
-
-function getAllNotificationsSettings (): UserNotificationSetting {
-  return {
-    newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
-  }
+  await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
 }
 
 async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
@@ -719,7 +640,7 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
       limit: 20
     }
   }
-  const servers = await flushAndRunMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
+  const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
 
   await setAccessTokensToServers(servers)
 
@@ -727,42 +648,33 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
     await doubleFollow(servers[0], servers[1])
   }
 
-  const user = {
-    username: 'user_1',
-    password: 'super password'
-  }
-  await createUser({
-    url: servers[0].url,
-    accessToken: servers[0].accessToken,
-    username: user.username,
-    password: user.password,
-    videoQuota: 10 * 1000 * 1000
-  })
-  const userAccessToken = await userLogin(servers[0], user)
+  const user = { username: 'user_1', password: 'super password' }
+  await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 })
+  const userAccessToken = await servers[0].login.getAccessToken(user)
 
-  await updateMyNotificationSettings(servers[0].url, userAccessToken, getAllNotificationsSettings())
-  await updateMyNotificationSettings(servers[0].url, servers[0].accessToken, getAllNotificationsSettings())
+  await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() })
+  await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
 
   if (serversCount > 1) {
-    await updateMyNotificationSettings(servers[1].url, servers[1].accessToken, getAllNotificationsSettings())
+    await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
   }
 
   {
-    const socket = getUserNotificationSocket(servers[0].url, userAccessToken)
+    const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken })
     socket.on('new-notification', n => userNotifications.push(n))
   }
   {
-    const socket = getUserNotificationSocket(servers[0].url, servers[0].accessToken)
+    const socket = servers[0].socketIO.getUserNotificationSocket()
     socket.on('new-notification', n => adminNotifications.push(n))
   }
 
   if (serversCount > 1) {
-    const socket = getUserNotificationSocket(servers[1].url, servers[1].accessToken)
+    const socket = servers[1].socketIO.getUserNotificationSocket()
     socket.on('new-notification', n => adminNotificationsServer2.push(n))
   }
 
-  const resChannel = await getMyUserInformation(servers[0].url, servers[0].accessToken)
-  const channelId = resChannel.body.videoChannels[0].id
+  const { videoChannels } = await servers[0].users.getMyInfo()
+  const channelId = videoChannels[0].id
 
   return {
     userNotifications,
@@ -778,11 +690,10 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
 // ---------------------------------------------------------------------------
 
 export {
+  getAllNotificationsSettings,
+
   CheckerBaseParams,
   CheckerType,
-  getAllNotificationsSettings,
-  checkNotification,
-  markAsReadAllNotifications,
   checkMyVideoImportIsFinished,
   checkUserRegistered,
   checkAutoInstanceFollowing,
@@ -792,14 +703,10 @@ export {
   checkNewCommentOnMyVideo,
   checkNewBlacklistOnMyVideo,
   checkCommentMention,
-  updateMyNotificationSettings,
   checkNewVideoAbuseForModerators,
   checkVideoAutoBlacklistForModerators,
   checkNewAbuseMessage,
   checkAbuseStateChange,
-  getUserNotifications,
-  markAsReadNotifications,
-  getLastNotification,
   checkNewInstanceFollower,
   prepareNotificationsTest,
   checkNewCommentAbuseForModerators,
@@ -807,3 +714,82 @@ export {
   checkNewPeerTubeVersion,
   checkNewPluginVersion
 }
+
+// ---------------------------------------------------------------------------
+
+async function checkNotification (options: CheckerBaseParams & {
+  notificationChecker: (notification: UserNotification, checkType: CheckerType) => void
+  emailNotificationFinder: (email: object) => boolean
+  checkType: CheckerType
+}) {
+  const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options
+
+  const check = options.check || { web: true, mail: true }
+
+  if (check.web) {
+    const notification = await server.notifications.getLastest({ token: token })
+
+    if (notification || checkType !== 'absence') {
+      notificationChecker(notification, checkType)
+    }
+
+    const socketNotification = socketNotifications.find(n => {
+      try {
+        notificationChecker(n, 'presence')
+        return true
+      } catch {
+        return false
+      }
+    })
+
+    if (checkType === 'presence') {
+      const obj = inspect(socketNotifications, { depth: 5 })
+      expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined
+    } else {
+      const obj = inspect(socketNotification, { depth: 5 })
+      expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined
+    }
+  }
+
+  if (check.mail) {
+    // Last email
+    const email = emails
+                      .slice()
+                      .reverse()
+                      .find(e => emailNotificationFinder(e))
+
+    if (checkType === 'presence') {
+      const texts = emails.map(e => e.text)
+      expect(email, 'The email is absent when is should be present. ' + inspect(texts)).to.not.be.undefined
+    } else {
+      expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
+    }
+  }
+}
+
+function checkVideo (video: any, videoName?: string, shortUUID?: string) {
+  if (videoName) {
+    expect(video.name).to.be.a('string')
+    expect(video.name).to.not.be.empty
+    expect(video.name).to.equal(videoName)
+  }
+
+  if (shortUUID) {
+    expect(video.shortUUID).to.be.a('string')
+    expect(video.shortUUID).to.not.be.empty
+    expect(video.shortUUID).to.equal(shortUUID)
+  }
+
+  expect(video.id).to.be.a('number')
+}
+
+function checkActor (actor: any) {
+  expect(actor.displayName).to.be.a('string')
+  expect(actor.displayName).to.not.be.empty
+  expect(actor.host).to.not.be.undefined
+}
+
+function checkComment (comment: any, commentId: number, threadId: number) {
+  expect(comment.id).to.equal(commentId)
+  expect(comment.threadId).to.equal(threadId)
+}
diff --git a/shared/extra-utils/users/subscriptions-command.ts b/shared/extra-utils/users/subscriptions-command.ts
new file mode 100644 (file)
index 0000000..edc60e6
--- /dev/null
@@ -0,0 +1,99 @@
+import { HttpStatusCode, ResultList, Video, VideoChannel } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class SubscriptionsCommand extends AbstractCommand {
+
+  add (options: OverrideCommandOptions & {
+    targetUri: string
+  }) {
+    const path = '/api/v1/users/me/subscriptions'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { uri: options.targetUri },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  list (options: OverrideCommandOptions & {
+    sort?: string // default -createdAt
+    search?: string
+  } = {}) {
+    const { sort = '-createdAt', search } = options
+    const path = '/api/v1/users/me/subscriptions'
+
+    return this.getRequestBody<ResultList<VideoChannel>>({
+      ...options,
+
+      path,
+      query: {
+        sort,
+        search
+      },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listVideos (options: OverrideCommandOptions & {
+    sort?: string // default -createdAt
+  } = {}) {
+    const { sort = '-createdAt' } = options
+    const path = '/api/v1/users/me/subscriptions/videos'
+
+    return this.getRequestBody<ResultList<Video>>({
+      ...options,
+
+      path,
+      query: { sort },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  get (options: OverrideCommandOptions & {
+    uri: string
+  }) {
+    const path = '/api/v1/users/me/subscriptions/' + options.uri
+
+    return this.getRequestBody<VideoChannel>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  remove (options: OverrideCommandOptions & {
+    uri: string
+  }) {
+    const path = '/api/v1/users/me/subscriptions/' + options.uri
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  exist (options: OverrideCommandOptions & {
+    uris: string[]
+  }) {
+    const path = '/api/v1/users/me/subscriptions/exist'
+
+    return this.getRequestBody<{ [id: string ]: boolean }>({
+      ...options,
+
+      path,
+      query: { 'uris[]': options.uris },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
diff --git a/shared/extra-utils/users/user-subscriptions.ts b/shared/extra-utils/users/user-subscriptions.ts
deleted file mode 100644 (file)
index edc7a35..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-import { makeDeleteRequest, makeGetRequest, makePostBodyRequest } from '../requests/requests'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function addUserSubscription (url: string, token: string, targetUri: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/users/me/subscriptions'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected,
-    fields: { uri: targetUri }
-  })
-}
-
-function listUserSubscriptions (parameters: {
-  url: string
-  token: string
-  sort?: string
-  search?: string
-  statusCodeExpected?: number
-}) {
-  const { url, token, sort = '-createdAt', search, statusCodeExpected = HttpStatusCode.OK_200 } = parameters
-  const path = '/api/v1/users/me/subscriptions'
-
-  return makeGetRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected,
-    query: {
-      sort,
-      search
-    }
-  })
-}
-
-function listUserSubscriptionVideos (url: string, token: string, sort = '-createdAt', statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/me/subscriptions/videos'
-
-  return makeGetRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected,
-    query: { sort }
-  })
-}
-
-function getUserSubscription (url: string, token: string, uri: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/me/subscriptions/' + uri
-
-  return makeGetRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-function removeUserSubscription (url: string, token: string, uri: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/users/me/subscriptions/' + uri
-
-  return makeDeleteRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-function areSubscriptionsExist (url: string, token: string, uris: string[], statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/me/subscriptions/exist'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: { 'uris[]': uris },
-    token,
-    statusCodeExpected
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  areSubscriptionsExist,
-  addUserSubscription,
-  listUserSubscriptions,
-  getUserSubscription,
-  listUserSubscriptionVideos,
-  removeUserSubscription
-}
diff --git a/shared/extra-utils/users/users-command.ts b/shared/extra-utils/users/users-command.ts
new file mode 100644 (file)
index 0000000..ddd20d0
--- /dev/null
@@ -0,0 +1,415 @@
+import { omit } from 'lodash'
+import { pick } from '@shared/core-utils'
+import {
+  HttpStatusCode,
+  MyUser,
+  ResultList,
+  User,
+  UserAdminFlag,
+  UserCreateResult,
+  UserRole,
+  UserUpdate,
+  UserUpdateMe,
+  UserVideoQuota,
+  UserVideoRate
+} from '@shared/models'
+import { ScopedToken } from '@shared/models/users/user-scoped-token'
+import { unwrapBody } from '../requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class UsersCommand extends AbstractCommand {
+
+  askResetPassword (options: OverrideCommandOptions & {
+    email: string
+  }) {
+    const { email } = options
+    const path = '/api/v1/users/ask-reset-password'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { email },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  resetPassword (options: OverrideCommandOptions & {
+    userId: number
+    verificationString: string
+    password: string
+  }) {
+    const { userId, verificationString, password } = options
+    const path = '/api/v1/users/' + userId + '/reset-password'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { password, verificationString },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  askSendVerifyEmail (options: OverrideCommandOptions & {
+    email: string
+  }) {
+    const { email } = options
+    const path = '/api/v1/users/ask-send-verify-email'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { email },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  verifyEmail (options: OverrideCommandOptions & {
+    userId: number
+    verificationString: string
+    isPendingEmail?: boolean // default false
+  }) {
+    const { userId, verificationString, isPendingEmail = false } = options
+    const path = '/api/v1/users/' + userId + '/verify-email'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: {
+        verificationString,
+        isPendingEmail
+      },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  banUser (options: OverrideCommandOptions & {
+    userId: number
+    reason?: string
+  }) {
+    const { userId, reason } = options
+    const path = '/api/v1/users' + '/' + userId + '/block'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { reason },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  unbanUser (options: OverrideCommandOptions & {
+    userId: number
+  }) {
+    const { userId } = options
+    const path = '/api/v1/users' + '/' + userId + '/unblock'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  getMyScopedTokens (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/users/scoped-tokens'
+
+    return this.getRequestBody<ScopedToken>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  renewMyScopedTokens (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/users/scoped-tokens'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  create (options: OverrideCommandOptions & {
+    username: string
+    password?: string
+    videoQuota?: number
+    videoQuotaDaily?: number
+    role?: UserRole
+    adminFlags?: UserAdminFlag
+  }) {
+    const {
+      username,
+      adminFlags,
+      password = 'password',
+      videoQuota = 42000000,
+      videoQuotaDaily = -1,
+      role = UserRole.USER
+    } = options
+
+    const path = '/api/v1/users'
+
+    return unwrapBody<{ user: UserCreateResult }>(this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: {
+        username,
+        password,
+        role,
+        adminFlags,
+        email: username + '@example.com',
+        videoQuota,
+        videoQuotaDaily
+      },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })).then(res => res.user)
+  }
+
+  async generate (username: string) {
+    const password = 'password'
+    const user = await this.create({ username, password })
+
+    const token = await this.server.login.getAccessToken({ username, password })
+
+    const me = await this.getMyInfo({ token })
+
+    return {
+      token,
+      userId: user.id,
+      userChannelId: me.videoChannels[0].id
+    }
+  }
+
+  async generateUserAndToken (username: string) {
+    const password = 'password'
+    await this.create({ username, password })
+
+    return this.server.login.getAccessToken({ username, password })
+  }
+
+  register (options: OverrideCommandOptions & {
+    username: string
+    password?: string
+    displayName?: string
+    channel?: {
+      name: string
+      displayName: string
+    }
+  }) {
+    const { username, password = 'password', displayName, channel } = options
+    const path = '/api/v1/users/register'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: {
+        username,
+        password,
+        email: username + '@example.com',
+        displayName,
+        channel
+      },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  getMyInfo (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/users/me'
+
+    return this.getRequestBody<MyUser>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getMyQuotaUsed (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/users/me/video-quota-used'
+
+    return this.getRequestBody<UserVideoQuota>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getMyRating (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const { videoId } = options
+    const path = '/api/v1/users/me/videos/' + videoId + '/rating'
+
+    return this.getRequestBody<UserVideoRate>({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  deleteMe (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/users/me'
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  updateMe (options: OverrideCommandOptions & UserUpdateMe) {
+    const path = '/api/v1/users/me'
+
+    const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: toSend,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  updateMyAvatar (options: OverrideCommandOptions & {
+    fixture: string
+  }) {
+    const { fixture } = options
+    const path = '/api/v1/users/me/avatar/pick'
+
+    return this.updateImageRequest({
+      ...options,
+
+      path,
+      fixture,
+      fieldname: 'avatarfile',
+
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  get (options: OverrideCommandOptions & {
+    userId: number
+    withStats?: boolean // default false
+  }) {
+    const { userId, withStats } = options
+    const path = '/api/v1/users/' + userId
+
+    return this.getRequestBody<User>({
+      ...options,
+
+      path,
+      query: { withStats },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  list (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+    search?: string
+    blocked?: boolean
+  } = {}) {
+    const path = '/api/v1/users'
+
+    return this.getRequestBody<ResultList<User>>({
+      ...options,
+
+      path,
+      query: pick(options, [ 'start', 'count', 'sort', 'search', 'blocked' ]),
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  remove (options: OverrideCommandOptions & {
+    userId: number
+  }) {
+    const { userId } = options
+    const path = '/api/v1/users/' + userId
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  update (options: OverrideCommandOptions & {
+    userId: number
+    email?: string
+    emailVerified?: boolean
+    videoQuota?: number
+    videoQuotaDaily?: number
+    password?: string
+    adminFlags?: UserAdminFlag
+    pluginAuth?: string
+    role?: UserRole
+  }) {
+    const path = '/api/v1/users/' + options.userId
+
+    const toSend: UserUpdate = {}
+    if (options.password !== undefined && options.password !== null) toSend.password = options.password
+    if (options.email !== undefined && options.email !== null) toSend.email = options.email
+    if (options.emailVerified !== undefined && options.emailVerified !== null) toSend.emailVerified = options.emailVerified
+    if (options.videoQuota !== undefined && options.videoQuota !== null) toSend.videoQuota = options.videoQuota
+    if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend.videoQuotaDaily = options.videoQuotaDaily
+    if (options.role !== undefined && options.role !== null) toSend.role = options.role
+    if (options.adminFlags !== undefined && options.adminFlags !== null) toSend.adminFlags = options.adminFlags
+    if (options.pluginAuth !== undefined) toSend.pluginAuth = options.pluginAuth
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: toSend,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts
deleted file mode 100644 (file)
index 0f15962..0000000
+++ /dev/null
@@ -1,415 +0,0 @@
-import { omit } from 'lodash'
-import * as request from 'supertest'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-import { UserUpdateMe } from '../../models/users'
-import { UserAdminFlag } from '../../models/users/user-flag.model'
-import { UserRegister } from '../../models/users/user-register.model'
-import { UserRole } from '../../models/users/user-role'
-import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateImageRequest } from '../requests/requests'
-import { ServerInfo } from '../server/servers'
-import { userLogin } from './login'
-
-function createUser (parameters: {
-  url: string
-  accessToken: string
-  username: string
-  password: string
-  videoQuota?: number
-  videoQuotaDaily?: number
-  role?: UserRole
-  adminFlags?: UserAdminFlag
-  specialStatus?: number
-}) {
-  const {
-    url,
-    accessToken,
-    username,
-    adminFlags,
-    password = 'password',
-    videoQuota = 1000000,
-    videoQuotaDaily = -1,
-    role = UserRole.USER,
-    specialStatus = HttpStatusCode.OK_200
-  } = parameters
-
-  const path = '/api/v1/users'
-  const body = {
-    username,
-    password,
-    role,
-    adminFlags,
-    email: username + '@example.com',
-    videoQuota,
-    videoQuotaDaily
-  }
-
-  return request(url)
-          .post(path)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + accessToken)
-          .send(body)
-          .expect(specialStatus)
-}
-
-async function generateUser (server: ServerInfo, username: string) {
-  const password = 'my super password'
-  const resCreate = await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
-
-  const token = await userLogin(server, { username, password })
-
-  const resMe = await getMyUserInformation(server.url, token)
-
-  return {
-    token,
-    userId: resCreate.body.user.id,
-    userChannelId: resMe.body.videoChannels[0].id
-  }
-}
-
-async function generateUserAccessToken (server: ServerInfo, username: string) {
-  const password = 'my super password'
-  await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
-
-  return userLogin(server, { username, password })
-}
-
-function registerUser (url: string, username: string, password: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/users/register'
-  const body = {
-    username,
-    password,
-    email: username + '@example.com'
-  }
-
-  return request(url)
-          .post(path)
-          .set('Accept', 'application/json')
-          .send(body)
-          .expect(specialStatus)
-}
-
-function registerUserWithChannel (options: {
-  url: string
-  user: { username: string, password: string, displayName?: string }
-  channel: { name: string, displayName: string }
-}) {
-  const path = '/api/v1/users/register'
-  const body: UserRegister = {
-    username: options.user.username,
-    password: options.user.password,
-    email: options.user.username + '@example.com',
-    channel: options.channel
-  }
-
-  if (options.user.displayName) {
-    Object.assign(body, { displayName: options.user.displayName })
-  }
-
-  return makePostBodyRequest({
-    url: options.url,
-    path,
-    fields: body,
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-function getMyUserInformation (url: string, accessToken: string, specialStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/me'
-
-  return request(url)
-          .get(path)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + accessToken)
-          .expect(specialStatus)
-          .expect('Content-Type', /json/)
-}
-
-function getUserScopedTokens (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/scoped-tokens'
-
-  return makeGetRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-function renewUserScopedTokens (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/scoped-tokens'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-function deleteMe (url: string, accessToken: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/users/me'
-
-  return request(url)
-    .delete(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(specialStatus)
-}
-
-function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/me/video-quota-used'
-
-  return request(url)
-          .get(path)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + accessToken)
-          .expect(specialStatus)
-          .expect('Content-Type', /json/)
-}
-
-function getUserInformation (url: string, accessToken: string, userId: number, withStats = false) {
-  const path = '/api/v1/users/' + userId
-
-  return request(url)
-    .get(path)
-    .query({ withStats })
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getMyUserVideoRating (url: string, accessToken: string, videoId: number | string, specialStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/users/me/videos/' + videoId + '/rating'
-
-  return request(url)
-          .get(path)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + accessToken)
-          .expect(specialStatus)
-          .expect('Content-Type', /json/)
-}
-
-function getUsersList (url: string, accessToken: string) {
-  const path = '/api/v1/users'
-
-  return request(url)
-          .get(path)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + accessToken)
-          .expect(HttpStatusCode.OK_200)
-          .expect('Content-Type', /json/)
-}
-
-function getUsersListPaginationAndSort (
-  url: string,
-  accessToken: string,
-  start: number,
-  count: number,
-  sort: string,
-  search?: string,
-  blocked?: boolean
-) {
-  const path = '/api/v1/users'
-
-  const query = {
-    start,
-    count,
-    sort,
-    search,
-    blocked
-  }
-
-  return request(url)
-          .get(path)
-          .query(query)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + accessToken)
-          .expect(HttpStatusCode.OK_200)
-          .expect('Content-Type', /json/)
-}
-
-function removeUser (url: string, userId: number | string, accessToken: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/users'
-
-  return request(url)
-          .delete(path + '/' + userId)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + accessToken)
-          .expect(expectedStatus)
-}
-
-function blockUser (
-  url: string,
-  userId: number | string,
-  accessToken: string,
-  expectedStatus = HttpStatusCode.NO_CONTENT_204,
-  reason?: string
-) {
-  const path = '/api/v1/users'
-  let body: any
-  if (reason) body = { reason }
-
-  return request(url)
-    .post(path + '/' + userId + '/block')
-    .send(body)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(expectedStatus)
-}
-
-function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/users'
-
-  return request(url)
-    .post(path + '/' + userId + '/unblock')
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(expectedStatus)
-}
-
-function updateMyUser (options: { url: string, accessToken: string, statusCodeExpected?: HttpStatusCode } & UserUpdateMe) {
-  const path = '/api/v1/users/me'
-
-  const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
-
-  return makePutBodyRequest({
-    url: options.url,
-    path,
-    token: options.accessToken,
-    fields: toSend,
-    statusCodeExpected: options.statusCodeExpected || HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-function updateMyAvatar (options: {
-  url: string
-  accessToken: string
-  fixture: string
-}) {
-  const path = '/api/v1/users/me/avatar/pick'
-
-  return updateImageRequest({ ...options, path, fieldname: 'avatarfile' })
-}
-
-function updateUser (options: {
-  url: string
-  userId: number
-  accessToken: string
-  email?: string
-  emailVerified?: boolean
-  videoQuota?: number
-  videoQuotaDaily?: number
-  password?: string
-  adminFlags?: UserAdminFlag
-  pluginAuth?: string
-  role?: UserRole
-}) {
-  const path = '/api/v1/users/' + options.userId
-
-  const toSend = {}
-  if (options.password !== undefined && options.password !== null) toSend['password'] = options.password
-  if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
-  if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified
-  if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota
-  if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily
-  if (options.role !== undefined && options.role !== null) toSend['role'] = options.role
-  if (options.adminFlags !== undefined && options.adminFlags !== null) toSend['adminFlags'] = options.adminFlags
-  if (options.pluginAuth !== undefined) toSend['pluginAuth'] = options.pluginAuth
-
-  return makePutBodyRequest({
-    url: options.url,
-    path,
-    token: options.accessToken,
-    fields: toSend,
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-function askResetPassword (url: string, email: string) {
-  const path = '/api/v1/users/ask-reset-password'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    fields: { email },
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-function resetPassword (
-  url: string,
-  userId: number,
-  verificationString: string,
-  password: string,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/users/' + userId + '/reset-password'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    fields: { password, verificationString },
-    statusCodeExpected
-  })
-}
-
-function askSendVerifyEmail (url: string, email: string) {
-  const path = '/api/v1/users/ask-send-verify-email'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    fields: { email },
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-function verifyEmail (
-  url: string,
-  userId: number,
-  verificationString: string,
-  isPendingEmail = false,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/users/' + userId + '/verify-email'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    fields: {
-      verificationString,
-      isPendingEmail
-    },
-    statusCodeExpected
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  createUser,
-  registerUser,
-  getMyUserInformation,
-  getMyUserVideoRating,
-  deleteMe,
-  registerUserWithChannel,
-  getMyUserVideoQuotaUsed,
-  getUsersList,
-  getUsersListPaginationAndSort,
-  removeUser,
-  updateUser,
-  updateMyUser,
-  getUserInformation,
-  blockUser,
-  unblockUser,
-  askResetPassword,
-  resetPassword,
-  renewUserScopedTokens,
-  updateMyAvatar,
-  generateUser,
-  askSendVerifyEmail,
-  generateUserAccessToken,
-  verifyEmail,
-  getUserScopedTokens
-}
diff --git a/shared/extra-utils/videos/blacklist-command.ts b/shared/extra-utils/videos/blacklist-command.ts
new file mode 100644 (file)
index 0000000..3a2ef89
--- /dev/null
@@ -0,0 +1,76 @@
+
+import { HttpStatusCode, ResultList } from '@shared/models'
+import { VideoBlacklist, VideoBlacklistType } from '../../models/videos'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class BlacklistCommand extends AbstractCommand {
+
+  add (options: OverrideCommandOptions & {
+    videoId: number | string
+    reason?: string
+    unfederate?: boolean
+  }) {
+    const { videoId, reason, unfederate } = options
+    const path = '/api/v1/videos/' + videoId + '/blacklist'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { reason, unfederate },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  update (options: OverrideCommandOptions & {
+    videoId: number | string
+    reason?: string
+  }) {
+    const { videoId, reason } = options
+    const path = '/api/v1/videos/' + videoId + '/blacklist'
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: { reason },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  remove (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const { videoId } = options
+    const path = '/api/v1/videos/' + videoId + '/blacklist'
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  list (options: OverrideCommandOptions & {
+    sort?: string
+    type?: VideoBlacklistType
+  } = {}) {
+    const { sort, type } = options
+    const path = '/api/v1/videos/blacklist/'
+
+    const query = { sort, type }
+
+    return this.getRequestBody<ResultList<VideoBlacklist>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
diff --git a/shared/extra-utils/videos/captions-command.ts b/shared/extra-utils/videos/captions-command.ts
new file mode 100644 (file)
index 0000000..a65ea99
--- /dev/null
@@ -0,0 +1,65 @@
+import { HttpStatusCode, ResultList, VideoCaption } from '@shared/models'
+import { buildAbsoluteFixturePath } from '../miscs'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class CaptionsCommand extends AbstractCommand {
+
+  add (options: OverrideCommandOptions & {
+    videoId: string | number
+    language: string
+    fixture: string
+    mimeType?: string
+  }) {
+    const { videoId, language, fixture, mimeType } = options
+
+    const path = '/api/v1/videos/' + videoId + '/captions/' + language
+
+    const captionfile = buildAbsoluteFixturePath(fixture)
+    const captionfileAttach = mimeType
+      ? [ captionfile, { contentType: mimeType } ]
+      : captionfile
+
+    return this.putUploadRequest({
+      ...options,
+
+      path,
+      fields: {},
+      attaches: {
+        captionfile: captionfileAttach
+      },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  list (options: OverrideCommandOptions & {
+    videoId: string | number
+  }) {
+    const { videoId } = options
+    const path = '/api/v1/videos/' + videoId + '/captions'
+
+    return this.getRequestBody<ResultList<VideoCaption>>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  delete (options: OverrideCommandOptions & {
+    videoId: string | number
+    language: string
+  }) {
+    const { videoId, language } = options
+    const path = '/api/v1/videos/' + videoId + '/captions/' + language
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/videos/captions.ts b/shared/extra-utils/videos/captions.ts
new file mode 100644 (file)
index 0000000..ff8a433
--- /dev/null
@@ -0,0 +1,17 @@
+import { expect } from 'chai'
+import * as request from 'supertest'
+import { HttpStatusCode } from '@shared/models'
+
+async function testCaptionFile (url: string, captionPath: string, containsString: string) {
+  const res = await request(url)
+    .get(captionPath)
+    .expect(HttpStatusCode.OK_200)
+
+  expect(res.text).to.contain(containsString)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  testCaptionFile
+}
diff --git a/shared/extra-utils/videos/change-ownership-command.ts b/shared/extra-utils/videos/change-ownership-command.ts
new file mode 100644 (file)
index 0000000..ad4c726
--- /dev/null
@@ -0,0 +1,68 @@
+
+import { HttpStatusCode, ResultList, VideoChangeOwnership } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class ChangeOwnershipCommand extends AbstractCommand {
+
+  create (options: OverrideCommandOptions & {
+    videoId: number | string
+    username: string
+  }) {
+    const { videoId, username } = options
+    const path = '/api/v1/videos/' + videoId + '/give-ownership'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { username },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  list (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/videos/ownership'
+
+    return this.getRequestBody<ResultList<VideoChangeOwnership>>({
+      ...options,
+
+      path,
+      query: { sort: '-createdAt' },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  accept (options: OverrideCommandOptions & {
+    ownershipId: number
+    channelId: number
+  }) {
+    const { ownershipId, channelId } = options
+    const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { channelId },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  refuse (options: OverrideCommandOptions & {
+    ownershipId: number
+  }) {
+    const { ownershipId } = options
+    const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/videos/channels-command.ts b/shared/extra-utils/videos/channels-command.ts
new file mode 100644 (file)
index 0000000..255e1d6
--- /dev/null
@@ -0,0 +1,156 @@
+import { pick } from '@shared/core-utils'
+import { HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models'
+import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
+import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
+import { unwrapBody } from '../requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class ChannelsCommand extends AbstractCommand {
+
+  list (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+    withStats?: boolean
+  } = {}) {
+    const path = '/api/v1/video-channels'
+
+    return this.getRequestBody<ResultList<VideoChannel>>({
+      ...options,
+
+      path,
+      query: pick(options, [ 'start', 'count', 'sort', 'withStats' ]),
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listByAccount (options: OverrideCommandOptions & {
+    accountName: string
+    start?: number
+    count?: number
+    sort?: string
+    withStats?: boolean
+    search?: string
+  }) {
+    const { accountName, sort = 'createdAt' } = options
+    const path = '/api/v1/accounts/' + accountName + '/video-channels'
+
+    return this.getRequestBody<ResultList<VideoChannel>>({
+      ...options,
+
+      path,
+      query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  async create (options: OverrideCommandOptions & {
+    attributes: VideoChannelCreate
+  }) {
+    const path = '/api/v1/video-channels/'
+
+    // Default attributes
+    const defaultAttributes = {
+      displayName: 'my super video channel',
+      description: 'my super channel description',
+      support: 'my super channel support'
+    }
+    const attributes = { ...defaultAttributes, ...options.attributes }
+
+    const body = await unwrapBody<{ videoChannel: VideoChannelCreateResult }>(this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: attributes,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+
+    return body.videoChannel
+  }
+
+  update (options: OverrideCommandOptions & {
+    channelName: string
+    attributes: VideoChannelUpdate
+  }) {
+    const { channelName, attributes } = options
+    const path = '/api/v1/video-channels/' + channelName
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: attributes,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  delete (options: OverrideCommandOptions & {
+    channelName: string
+  }) {
+    const path = '/api/v1/video-channels/' + options.channelName
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  get (options: OverrideCommandOptions & {
+    channelName: string
+  }) {
+    const path = '/api/v1/video-channels/' + options.channelName
+
+    return this.getRequestBody<VideoChannel>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  updateImage (options: OverrideCommandOptions & {
+    fixture: string
+    channelName: string | number
+    type: 'avatar' | 'banner'
+  }) {
+    const { channelName, fixture, type } = options
+
+    const path = `/api/v1/video-channels/${channelName}/${type}/pick`
+
+    return this.updateImageRequest({
+      ...options,
+
+      path,
+      fixture,
+      fieldname: type + 'file',
+
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  deleteImage (options: OverrideCommandOptions & {
+    channelName: string | number
+    type: 'avatar' | 'banner'
+  }) {
+    const { channelName, type } = options
+
+    const path = `/api/v1/video-channels/${channelName}/${type}`
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/videos/channels.ts b/shared/extra-utils/videos/channels.ts
new file mode 100644 (file)
index 0000000..756c474
--- /dev/null
@@ -0,0 +1,18 @@
+import { PeerTubeServer } from '../server/server'
+
+function setDefaultVideoChannel (servers: PeerTubeServer[]) {
+  const tasks: Promise<any>[] = []
+
+  for (const server of servers) {
+    const p = server.users.getMyInfo()
+      .then(user => { server.store.channel = user.videoChannels[0] })
+
+    tasks.push(p)
+  }
+
+  return Promise.all(tasks)
+}
+
+export {
+  setDefaultVideoChannel
+}
diff --git a/shared/extra-utils/videos/comments-command.ts b/shared/extra-utils/videos/comments-command.ts
new file mode 100644 (file)
index 0000000..f0d163a
--- /dev/null
@@ -0,0 +1,152 @@
+import { pick } from 'lodash'
+import { HttpStatusCode, ResultList, VideoComment, VideoCommentThreads, VideoCommentThreadTree } from '@shared/models'
+import { unwrapBody } from '../requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class CommentsCommand extends AbstractCommand {
+
+  private lastVideoId: number | string
+  private lastThreadId: number
+  private lastReplyId: number
+
+  listForAdmin (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+    isLocal?: boolean
+    search?: string
+    searchAccount?: string
+    searchVideo?: string
+  } = {}) {
+    const { sort = '-createdAt' } = options
+    const path = '/api/v1/videos/comments'
+
+    const query = { sort, ...pick(options, [ 'start', 'count', 'isLocal', 'search', 'searchAccount', 'searchVideo' ]) }
+
+    return this.getRequestBody<ResultList<VideoComment>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listThreads (options: OverrideCommandOptions & {
+    videoId: number | string
+    start?: number
+    count?: number
+    sort?: string
+  }) {
+    const { start, count, sort, videoId } = options
+    const path = '/api/v1/videos/' + videoId + '/comment-threads'
+
+    return this.getRequestBody<VideoCommentThreads>({
+      ...options,
+
+      path,
+      query: { start, count, sort },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getThread (options: OverrideCommandOptions & {
+    videoId: number | string
+    threadId: number
+  }) {
+    const { videoId, threadId } = options
+    const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
+
+    return this.getRequestBody<VideoCommentThreadTree>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  async createThread (options: OverrideCommandOptions & {
+    videoId: number | string
+    text: string
+  }) {
+    const { videoId, text } = options
+    const path = '/api/v1/videos/' + videoId + '/comment-threads'
+
+    const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { text },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+
+    this.lastThreadId = body.comment?.id
+    this.lastVideoId = videoId
+
+    return body.comment
+  }
+
+  async addReply (options: OverrideCommandOptions & {
+    videoId: number | string
+    toCommentId: number
+    text: string
+  }) {
+    const { videoId, toCommentId, text } = options
+    const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId
+
+    const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { text },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+
+    this.lastReplyId = body.comment?.id
+
+    return body.comment
+  }
+
+  async addReplyToLastReply (options: OverrideCommandOptions & {
+    text: string
+  }) {
+    return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
+  }
+
+  async addReplyToLastThread (options: OverrideCommandOptions & {
+    text: string
+  }) {
+    return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
+  }
+
+  async findCommentId (options: OverrideCommandOptions & {
+    videoId: number | string
+    text: string
+  }) {
+    const { videoId, text } = options
+    const { data } = await this.listThreads({ videoId, count: 25, sort: '-createdAt' })
+
+    return data.find(c => c.text === text).id
+  }
+
+  delete (options: OverrideCommandOptions & {
+    videoId: number | string
+    commentId: number
+  }) {
+    const { videoId, commentId } = options
+    const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/videos/history-command.ts b/shared/extra-utils/videos/history-command.ts
new file mode 100644 (file)
index 0000000..13b7150
--- /dev/null
@@ -0,0 +1,58 @@
+import { HttpStatusCode, ResultList, Video } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class HistoryCommand extends AbstractCommand {
+
+  wathVideo (options: OverrideCommandOptions & {
+    videoId: number | string
+    currentTime: number
+  }) {
+    const { videoId, currentTime } = options
+
+    const path = '/api/v1/videos/' + videoId + '/watching'
+    const fields = { currentTime }
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  list (options: OverrideCommandOptions & {
+    search?: string
+  } = {}) {
+    const { search } = options
+    const path = '/api/v1/users/me/history/videos'
+
+    return this.getRequestBody<ResultList<Video>>({
+      ...options,
+
+      path,
+      query: {
+        search
+      },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  remove (options: OverrideCommandOptions & {
+    beforeDate?: string
+  } = {}) {
+    const { beforeDate } = options
+    const path = '/api/v1/users/me/history/videos/remove'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: { beforeDate },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
diff --git a/shared/extra-utils/videos/imports-command.ts b/shared/extra-utils/videos/imports-command.ts
new file mode 100644 (file)
index 0000000..e494469
--- /dev/null
@@ -0,0 +1,47 @@
+
+import { HttpStatusCode, ResultList } from '@shared/models'
+import { VideoImport, VideoImportCreate } from '../../models/videos'
+import { unwrapBody } from '../requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class ImportsCommand extends AbstractCommand {
+
+  importVideo (options: OverrideCommandOptions & {
+    attributes: VideoImportCreate & { torrentfile?: string }
+  }) {
+    const { attributes } = options
+    const path = '/api/v1/videos/imports'
+
+    let attaches: any = {}
+    if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
+
+    return unwrapBody<VideoImport>(this.postUploadRequest({
+      ...options,
+
+      path,
+      attaches,
+      fields: options.attributes,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  getMyVideoImports (options: OverrideCommandOptions & {
+    sort?: string
+  } = {}) {
+    const { sort } = options
+    const path = '/api/v1/users/me/videos/imports'
+
+    const query = {}
+    if (sort) query['sort'] = sort
+
+    return this.getRequestBody<ResultList<VideoImport>>({
+      ...options,
+
+      path,
+      query: { sort },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
diff --git a/shared/extra-utils/videos/index.ts b/shared/extra-utils/videos/index.ts
new file mode 100644 (file)
index 0000000..26e663f
--- /dev/null
@@ -0,0 +1,19 @@
+export * from './blacklist-command'
+export * from './captions-command'
+export * from './captions'
+export * from './change-ownership-command'
+export * from './channels'
+export * from './channels-command'
+export * from './comments-command'
+export * from './history-command'
+export * from './imports-command'
+export * from './live-command'
+export * from './live'
+export * from './playlists-command'
+export * from './playlists'
+export * from './services-command'
+export * from './streaming-playlists-command'
+export * from './streaming-playlists'
+export * from './comments-command'
+export * from './videos-command'
+export * from './videos'
diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts
new file mode 100644 (file)
index 0000000..bf9486a
--- /dev/null
@@ -0,0 +1,154 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { readdir } from 'fs-extra'
+import { omit } from 'lodash'
+import { join } from 'path'
+import { HttpStatusCode, LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoCreateResult, VideoDetails, VideoState } from '@shared/models'
+import { wait } from '../miscs'
+import { unwrapBody } from '../requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+import { sendRTMPStream, testFfmpegStreamError } from './live'
+
+export class LiveCommand extends AbstractCommand {
+
+  get (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const path = '/api/v1/videos/live'
+
+    return this.getRequestBody<LiveVideo>({
+      ...options,
+
+      path: path + '/' + options.videoId,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  update (options: OverrideCommandOptions & {
+    videoId: number | string
+    fields: LiveVideoUpdate
+  }) {
+    const { videoId, fields } = options
+    const path = '/api/v1/videos/live'
+
+    return this.putBodyRequest({
+      ...options,
+
+      path: path + '/' + videoId,
+      fields,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  async create (options: OverrideCommandOptions & {
+    fields: LiveVideoCreate
+  }) {
+    const { fields } = options
+    const path = '/api/v1/videos/live'
+
+    const attaches: any = {}
+    if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
+    if (fields.previewfile) attaches.previewfile = fields.previewfile
+
+    const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
+      ...options,
+
+      path,
+      attaches,
+      fields: omit(fields, 'thumbnailfile', 'previewfile'),
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+
+    return body.video
+  }
+
+  async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
+    videoId: number | string
+    fixtureName?: string
+  }) {
+    const { videoId, fixtureName } = options
+    const videoLive = await this.get({ videoId })
+
+    return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
+  }
+
+  async runAndTestStreamError (options: OverrideCommandOptions & {
+    videoId: number | string
+    shouldHaveError: boolean
+  }) {
+    const command = await this.sendRTMPStreamInVideo(options)
+
+    return testFfmpegStreamError(command, options.shouldHaveError)
+  }
+
+  waitUntilPublished (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const { videoId } = options
+    return this.waitUntilState({ videoId, state: VideoState.PUBLISHED })
+  }
+
+  waitUntilWaiting (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const { videoId } = options
+    return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE })
+  }
+
+  waitUntilEnded (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    const { videoId } = options
+    return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
+  }
+
+  waitUntilSegmentGeneration (options: OverrideCommandOptions & {
+    videoUUID: string
+    resolution: number
+    segment: number
+  }) {
+    const { resolution, segment, videoUUID } = options
+    const segmentName = `${resolution}-00000${segment}.ts`
+
+    return this.server.servers.waitUntilLog(`${videoUUID}/${segmentName}`, 2, false)
+  }
+
+  async waitUntilSaved (options: OverrideCommandOptions & {
+    videoId: number | string
+  }) {
+    let video: VideoDetails
+
+    do {
+      video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
+
+      await wait(500)
+    } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
+  }
+
+  async countPlaylists (options: OverrideCommandOptions & {
+    videoUUID: string
+  }) {
+    const basePath = this.server.servers.buildDirectory('streaming-playlists')
+    const hlsPath = join(basePath, 'hls', options.videoUUID)
+
+    const files = await readdir(hlsPath)
+
+    return files.filter(f => f.endsWith('.m3u8')).length
+  }
+
+  private async waitUntilState (options: OverrideCommandOptions & {
+    videoId: number | string
+    state: VideoState
+  }) {
+    let video: VideoDetails
+
+    do {
+      video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
+
+      await wait(500)
+    } while (video.state.id !== options.state)
+  }
+}
index c0384769bda247f4dc13ce8a38d70ac2c8879259..94f5f5b59a563fd0b96a37a4f984983aaf36c13d 100644 (file)
@@ -3,69 +3,9 @@
 import { expect } from 'chai'
 import * as ffmpeg from 'fluent-ffmpeg'
 import { pathExists, readdir } from 'fs-extra'
-import { omit } from 'lodash'
 import { join } from 'path'
-import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-import { buildAbsoluteFixturePath, buildServerDirectory, wait } from '../miscs/miscs'
-import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
-import { ServerInfo, waitUntilLog } from '../server/servers'
-import { getVideoWithToken } from './videos'
-
-function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/videos/live'
-
-  return makeGetRequest({
-    url,
-    token,
-    path: path + '/' + videoId,
-    statusCodeExpected
-  })
-}
-
-function updateLive (
-  url: string,
-  token: string,
-  videoId: number | string,
-  fields: LiveVideoUpdate,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/videos/live'
-
-  return makePutBodyRequest({
-    url,
-    token,
-    path: path + '/' + videoId,
-    fields,
-    statusCodeExpected
-  })
-}
-
-function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/videos/live'
-
-  const attaches: any = {}
-  if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
-  if (fields.previewfile) attaches.previewfile = fields.previewfile
-
-  const updatedFields = omit(fields, 'thumbnailfile', 'previewfile')
-
-  return makeUploadRequest({
-    url,
-    path,
-    token,
-    attaches,
-    fields: updatedFields,
-    statusCodeExpected
-  })
-}
-
-async function sendRTMPStreamInVideo (url: string, token: string, videoId: number | string, fixtureName?: string) {
-  const res = await getLive(url, token, videoId)
-  const videoLive = res.body as LiveVideo
-
-  return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
-}
+import { buildAbsoluteFixturePath, wait } from '../miscs'
+import { PeerTubeServer } from '../server/server'
 
 function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') {
   const fixture = buildAbsoluteFixturePath(fixtureName)
@@ -109,12 +49,6 @@ function waitFfmpegUntilError (command: ffmpeg.FfmpegCommand, successAfterMS = 1
   })
 }
 
-async function runAndTestFfmpegStreamError (url: string, token: string, videoId: number | string, shouldHaveError: boolean) {
-  const command = await sendRTMPStreamInVideo(url, token, videoId)
-
-  return testFfmpegStreamError(command, shouldHaveError)
-}
-
 async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) {
   let error: Error
 
@@ -136,53 +70,14 @@ async function stopFfmpeg (command: ffmpeg.FfmpegCommand) {
   await wait(500)
 }
 
-function waitUntilLivePublished (url: string, token: string, videoId: number | string) {
-  return waitUntilLiveState(url, token, videoId, VideoState.PUBLISHED)
-}
-
-function waitUntilLiveWaiting (url: string, token: string, videoId: number | string) {
-  return waitUntilLiveState(url, token, videoId, VideoState.WAITING_FOR_LIVE)
-}
-
-function waitUntilLiveEnded (url: string, token: string, videoId: number | string) {
-  return waitUntilLiveState(url, token, videoId, VideoState.LIVE_ENDED)
-}
-
-function waitUntilLiveSegmentGeneration (server: ServerInfo, videoUUID: string, resolutionNum: number, segmentNum: number) {
-  const segmentName = `${resolutionNum}-00000${segmentNum}.ts`
-  return waitUntilLog(server, `${videoUUID}/${segmentName}`, 2, false)
-}
-
-async function waitUntilLiveState (url: string, token: string, videoId: number | string, state: VideoState) {
-  let video: VideoDetails
-
-  do {
-    const res = await getVideoWithToken(url, token, videoId)
-    video = res.body
-
-    await wait(500)
-  } while (video.state.id !== state)
-}
-
-async function waitUntilLiveSaved (url: string, token: string, videoId: number | string) {
-  let video: VideoDetails
-
-  do {
-    const res = await getVideoWithToken(url, token, videoId)
-    video = res.body
-
-    await wait(500)
-  } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
-}
-
-async function waitUntilLivePublishedOnAllServers (servers: ServerInfo[], videoId: string) {
+async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
   for (const server of servers) {
-    await waitUntilLivePublished(server.url, server.accessToken, videoId)
+    await server.live.waitUntilPublished({ videoId })
   }
 }
 
-async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resolutions: number[] = []) {
-  const basePath = buildServerDirectory(server, 'streaming-playlists')
+async function checkLiveCleanupAfterSave (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) {
+  const basePath = server.servers.buildDirectory('streaming-playlists')
   const hlsPath = join(basePath, 'hls', videoUUID)
 
   if (resolutions.length === 0) {
@@ -198,41 +93,25 @@ async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resoluti
   expect(files).to.have.lengthOf(resolutions.length * 2 + 2)
 
   for (const resolution of resolutions) {
-    expect(files).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
-    expect(files).to.contain(`${resolution}.m3u8`)
-  }
-
-  expect(files).to.contain('master.m3u8')
-  expect(files).to.contain('segments-sha256.json')
-}
+    const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`))
+    expect(fragmentedFile).to.exist
 
-async function getPlaylistsCount (server: ServerInfo, videoUUID: string) {
-  const basePath = buildServerDirectory(server, 'streaming-playlists')
-  const hlsPath = join(basePath, 'hls', videoUUID)
+    const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`))
+    expect(playlistFile).to.exist
+  }
 
-  const files = await readdir(hlsPath)
+  const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8'))
+  expect(masterPlaylistFile).to.exist
 
-  return files.filter(f => f.endsWith('.m3u8')).length
+  const shaFile = files.find(f => f.endsWith('-segments-sha256.json'))
+  expect(shaFile).to.exist
 }
 
-// ---------------------------------------------------------------------------
-
 export {
-  getLive,
-  getPlaylistsCount,
-  waitUntilLiveSaved,
-  waitUntilLivePublished,
-  updateLive,
-  createLive,
-  runAndTestFfmpegStreamError,
-  checkLiveCleanup,
-  waitUntilLiveSegmentGeneration,
-  stopFfmpeg,
-  waitUntilLiveWaiting,
-  sendRTMPStreamInVideo,
-  waitUntilLiveEnded,
+  sendRTMPStream,
   waitFfmpegUntilError,
+  testFfmpegStreamError,
+  stopFfmpeg,
   waitUntilLivePublishedOnAllServers,
-  sendRTMPStream,
-  testFfmpegStreamError
+  checkLiveCleanupAfterSave
 }
diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts
new file mode 100644 (file)
index 0000000..ce23900
--- /dev/null
@@ -0,0 +1,280 @@
+import { omit } from 'lodash'
+import { pick } from '@shared/core-utils'
+import {
+  BooleanBothQuery,
+  HttpStatusCode,
+  ResultList,
+  VideoExistInPlaylist,
+  VideoPlaylist,
+  VideoPlaylistCreate,
+  VideoPlaylistCreateResult,
+  VideoPlaylistElement,
+  VideoPlaylistElementCreate,
+  VideoPlaylistElementCreateResult,
+  VideoPlaylistElementUpdate,
+  VideoPlaylistReorder,
+  VideoPlaylistType,
+  VideoPlaylistUpdate
+} from '@shared/models'
+import { unwrapBody } from '../requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class PlaylistsCommand extends AbstractCommand {
+
+  list (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+  }) {
+    const path = '/api/v1/video-playlists'
+    const query = pick(options, [ 'start', 'count', 'sort' ])
+
+    return this.getRequestBody<ResultList<VideoPlaylist>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listByChannel (options: OverrideCommandOptions & {
+    handle: string
+    start?: number
+    count?: number
+    sort?: string
+  }) {
+    const path = '/api/v1/video-channels/' + options.handle + '/video-playlists'
+    const query = pick(options, [ 'start', 'count', 'sort' ])
+
+    return this.getRequestBody<ResultList<VideoPlaylist>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listByAccount (options: OverrideCommandOptions & {
+    handle: string
+    start?: number
+    count?: number
+    sort?: string
+    search?: string
+    playlistType?: VideoPlaylistType
+  }) {
+    const path = '/api/v1/accounts/' + options.handle + '/video-playlists'
+    const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType' ])
+
+    return this.getRequestBody<ResultList<VideoPlaylist>>({
+      ...options,
+
+      path,
+      query,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  get (options: OverrideCommandOptions & {
+    playlistId: number | string
+  }) {
+    const { playlistId } = options
+    const path = '/api/v1/video-playlists/' + playlistId
+
+    return this.getRequestBody<VideoPlaylist>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listVideos (options: OverrideCommandOptions & {
+    playlistId: number | string
+    start?: number
+    count?: number
+    query?: { nsfw?: BooleanBothQuery }
+  }) {
+    const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
+    const query = options.query ?? {}
+
+    return this.getRequestBody<ResultList<VideoPlaylistElement>>({
+      ...options,
+
+      path,
+      query: {
+        ...query,
+        start: options.start,
+        count: options.count
+      },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  delete (options: OverrideCommandOptions & {
+    playlistId: number | string
+  }) {
+    const path = '/api/v1/video-playlists/' + options.playlistId
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  async create (options: OverrideCommandOptions & {
+    attributes: VideoPlaylistCreate
+  }) {
+    const path = '/api/v1/video-playlists'
+
+    const fields = omit(options.attributes, 'thumbnailfile')
+
+    const attaches = options.attributes.thumbnailfile
+      ? { thumbnailfile: options.attributes.thumbnailfile }
+      : {}
+
+    const body = await unwrapBody<{ videoPlaylist: VideoPlaylistCreateResult }>(this.postUploadRequest({
+      ...options,
+
+      path,
+      fields,
+      attaches,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+
+    return body.videoPlaylist
+  }
+
+  update (options: OverrideCommandOptions & {
+    attributes: VideoPlaylistUpdate
+    playlistId: number | string
+  }) {
+    const path = '/api/v1/video-playlists/' + options.playlistId
+
+    const fields = omit(options.attributes, 'thumbnailfile')
+
+    const attaches = options.attributes.thumbnailfile
+      ? { thumbnailfile: options.attributes.thumbnailfile }
+      : {}
+
+    return this.putUploadRequest({
+      ...options,
+
+      path,
+      fields,
+      attaches,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  async addElement (options: OverrideCommandOptions & {
+    playlistId: number | string
+    attributes: VideoPlaylistElementCreate | { videoId: string }
+  }) {
+    const attributes = {
+      ...options.attributes,
+
+      videoId: await this.server.videos.getId({ ...options, uuid: options.attributes.videoId })
+    }
+
+    const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
+
+    const body = await unwrapBody<{ videoPlaylistElement: VideoPlaylistElementCreateResult }>(this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: attributes,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+
+    return body.videoPlaylistElement
+  }
+
+  updateElement (options: OverrideCommandOptions & {
+    playlistId: number | string
+    elementId: number | string
+    attributes: VideoPlaylistElementUpdate
+  }) {
+    const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: options.attributes,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  removeElement (options: OverrideCommandOptions & {
+    playlistId: number | string
+    elementId: number
+  }) {
+    const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  reorderElements (options: OverrideCommandOptions & {
+    playlistId: number | string
+    attributes: VideoPlaylistReorder
+  }) {
+    const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: options.attributes,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  getPrivacies (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/video-playlists/privacies'
+
+    return this.getRequestBody<{ [ id: number ]: string }>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  videosExist (options: OverrideCommandOptions & {
+    videoIds: number[]
+  }) {
+    const { videoIds } = options
+    const path = '/api/v1/users/me/video-playlists/videos-exist'
+
+    return this.getRequestBody<VideoExistInPlaylist>({
+      ...options,
+
+      path,
+      query: { videoIds },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
diff --git a/shared/extra-utils/videos/playlists.ts b/shared/extra-utils/videos/playlists.ts
new file mode 100644 (file)
index 0000000..3dde52b
--- /dev/null
@@ -0,0 +1,25 @@
+import { expect } from 'chai'
+import { readdir } from 'fs-extra'
+import { join } from 'path'
+import { root } from '../miscs'
+
+async function checkPlaylistFilesWereRemoved (
+  playlistUUID: string,
+  internalServerNumber: number,
+  directories = [ 'thumbnails' ]
+) {
+  const testDirectory = 'test' + internalServerNumber
+
+  for (const directory of directories) {
+    const directoryPath = join(root(), testDirectory, directory)
+
+    const files = await readdir(directoryPath)
+    for (const file of files) {
+      expect(file).to.not.contain(playlistUUID)
+    }
+  }
+}
+
+export {
+  checkPlaylistFilesWereRemoved
+}
diff --git a/shared/extra-utils/videos/services-command.ts b/shared/extra-utils/videos/services-command.ts
new file mode 100644 (file)
index 0000000..06760df
--- /dev/null
@@ -0,0 +1,29 @@
+import { HttpStatusCode } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class ServicesCommand extends AbstractCommand {
+
+  getOEmbed (options: OverrideCommandOptions & {
+    oembedUrl: string
+    format?: string
+    maxHeight?: number
+    maxWidth?: number
+  }) {
+    const path = '/services/oembed'
+    const query = {
+      url: options.oembedUrl,
+      format: options.format,
+      maxheight: options.maxHeight,
+      maxwidth: options.maxWidth
+    }
+
+    return this.getRequest({
+      ...options,
+
+      path,
+      query,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+}
diff --git a/shared/extra-utils/videos/services.ts b/shared/extra-utils/videos/services.ts
deleted file mode 100644 (file)
index e13a788..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as request from 'supertest'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getOEmbed (url: string, oembedUrl: string, format?: string, maxHeight?: number, maxWidth?: number) {
-  const path = '/services/oembed'
-  const query = {
-    url: oembedUrl,
-    format,
-    maxheight: maxHeight,
-    maxwidth: maxWidth
-  }
-
-  return request(url)
-          .get(path)
-          .query(query)
-          .set('Accept', 'application/json')
-          .expect(HttpStatusCode.OK_200)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getOEmbed
-}
diff --git a/shared/extra-utils/videos/streaming-playlists-command.ts b/shared/extra-utils/videos/streaming-playlists-command.ts
new file mode 100644 (file)
index 0000000..9662685
--- /dev/null
@@ -0,0 +1,44 @@
+import { HttpStatusCode } from '@shared/models'
+import { unwrapBody, unwrapText } from '../requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class StreamingPlaylistsCommand extends AbstractCommand {
+
+  get (options: OverrideCommandOptions & {
+    url: string
+  }) {
+    return unwrapText(this.getRawRequest({
+      ...options,
+
+      url: options.url,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  getSegment (options: OverrideCommandOptions & {
+    url: string
+    range?: string
+  }) {
+    return unwrapBody<Buffer>(this.getRawRequest({
+      ...options,
+
+      url: options.url,
+      range: options.range,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  getSegmentSha256 (options: OverrideCommandOptions & {
+    url: string
+  }) {
+    return unwrapBody<{ [ id: string ]: string }>(this.getRawRequest({
+      ...options,
+
+      url: options.url,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+}
diff --git a/shared/extra-utils/videos/streaming-playlists.ts b/shared/extra-utils/videos/streaming-playlists.ts
new file mode 100644 (file)
index 0000000..a224b8f
--- /dev/null
@@ -0,0 +1,78 @@
+import { expect } from 'chai'
+import { basename } from 'path'
+import { sha256 } from '@server/helpers/core-utils'
+import { removeFragmentedMP4Ext } from '@shared/core-utils'
+import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models'
+import { PeerTubeServer } from '../server'
+
+async function checkSegmentHash (options: {
+  server: PeerTubeServer
+  baseUrlPlaylist: string
+  baseUrlSegment: string
+  videoUUID: string
+  resolution: number
+  hlsPlaylist: VideoStreamingPlaylist
+}) {
+  const { server, baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist } = options
+  const command = server.streamingPlaylists
+
+  const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
+  const videoName = basename(file.fileUrl)
+
+  const playlist = await command.get({ url: `${baseUrlPlaylist}/${videoUUID}/${removeFragmentedMP4Ext(videoName)}.m3u8` })
+
+  const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
+
+  const length = parseInt(matches[1], 10)
+  const offset = parseInt(matches[2], 10)
+  const range = `${offset}-${offset + length - 1}`
+
+  const segmentBody = await command.getSegment({
+    url: `${baseUrlSegment}/${videoUUID}/${videoName}`,
+    expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
+    range: `bytes=${range}`
+  })
+
+  const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
+  expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
+}
+
+async function checkLiveSegmentHash (options: {
+  server: PeerTubeServer
+  baseUrlSegment: string
+  videoUUID: string
+  segmentName: string
+  hlsPlaylist: VideoStreamingPlaylist
+}) {
+  const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options
+  const command = server.streamingPlaylists
+
+  const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` })
+  const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
+
+  expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
+}
+
+async function checkResolutionsInMasterPlaylist (options: {
+  server: PeerTubeServer
+  playlistUrl: string
+  resolutions: number[]
+}) {
+  const { server, playlistUrl, resolutions } = options
+
+  const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl })
+
+  for (const resolution of resolutions) {
+    const reg = new RegExp(
+      '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
+    )
+
+    expect(masterPlaylist).to.match(reg)
+  }
+}
+
+export {
+  checkSegmentHash,
+  checkLiveSegmentHash,
+  checkResolutionsInMasterPlaylist
+}
diff --git a/shared/extra-utils/videos/video-blacklist.ts b/shared/extra-utils/videos/video-blacklist.ts
deleted file mode 100644 (file)
index aa15485..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-import * as request from 'supertest'
-import { VideoBlacklistType } from '../../models/videos'
-import { makeGetRequest } from '..'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function addVideoToBlacklist (
-  url: string,
-  token: string,
-  videoId: number | string,
-  reason?: string,
-  unfederate?: boolean,
-  specialStatus = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/videos/' + videoId + '/blacklist'
-
-  return request(url)
-    .post(path)
-    .send({ reason, unfederate })
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(specialStatus)
-}
-
-function updateVideoBlacklist (
-  url: string,
-  token: string,
-  videoId: number,
-  reason?: string,
-  specialStatus = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/videos/' + videoId + '/blacklist'
-
-  return request(url)
-    .put(path)
-    .send({ reason })
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(specialStatus)
-}
-
-function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/videos/' + videoId + '/blacklist'
-
-  return request(url)
-    .delete(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(specialStatus)
-}
-
-function getBlacklistedVideosList (parameters: {
-  url: string
-  token: string
-  sort?: string
-  type?: VideoBlacklistType
-  specialStatus?: HttpStatusCode
-}) {
-  const { url, token, sort, type, specialStatus = HttpStatusCode.OK_200 } = parameters
-  const path = '/api/v1/videos/blacklist/'
-
-  const query = { sort, type }
-
-  return makeGetRequest({
-    url,
-    path,
-    query,
-    token,
-    statusCodeExpected: specialStatus
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  addVideoToBlacklist,
-  removeVideoFromBlacklist,
-  getBlacklistedVideosList,
-  updateVideoBlacklist
-}
diff --git a/shared/extra-utils/videos/video-captions.ts b/shared/extra-utils/videos/video-captions.ts
deleted file mode 100644 (file)
index 62eec7b..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-import { makeDeleteRequest, makeGetRequest, makeUploadRequest } from '../requests/requests'
-import * as request from 'supertest'
-import * as chai from 'chai'
-import { buildAbsoluteFixturePath } from '../miscs/miscs'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-const expect = chai.expect
-
-function createVideoCaption (args: {
-  url: string
-  accessToken: string
-  videoId: string | number
-  language: string
-  fixture: string
-  mimeType?: string
-  statusCodeExpected?: number
-}) {
-  const path = '/api/v1/videos/' + args.videoId + '/captions/' + args.language
-
-  const captionfile = buildAbsoluteFixturePath(args.fixture)
-  const captionfileAttach = args.mimeType ? [ captionfile, { contentType: args.mimeType } ] : captionfile
-
-  return makeUploadRequest({
-    method: 'PUT',
-    url: args.url,
-    path,
-    token: args.accessToken,
-    fields: {},
-    attaches: {
-      captionfile: captionfileAttach
-    },
-    statusCodeExpected: args.statusCodeExpected || HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-function listVideoCaptions (url: string, videoId: string | number) {
-  const path = '/api/v1/videos/' + videoId + '/captions'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function deleteVideoCaption (url: string, token: string, videoId: string | number, language: string) {
-  const path = '/api/v1/videos/' + videoId + '/captions/' + language
-
-  return makeDeleteRequest({
-    url,
-    token,
-    path,
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-async function testCaptionFile (url: string, captionPath: string, containsString: string) {
-  const res = await request(url)
-    .get(captionPath)
-    .expect(HttpStatusCode.OK_200)
-
-  expect(res.text).to.contain(containsString)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  createVideoCaption,
-  listVideoCaptions,
-  testCaptionFile,
-  deleteVideoCaption
-}
diff --git a/shared/extra-utils/videos/video-change-ownership.ts b/shared/extra-utils/videos/video-change-ownership.ts
deleted file mode 100644 (file)
index ef82a76..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-import * as request from 'supertest'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function changeVideoOwnership (
-  url: string,
-  token: string,
-  videoId: number | string,
-  username,
-  expectedStatus = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/videos/' + videoId + '/give-ownership'
-
-  return request(url)
-    .post(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .send({ username })
-    .expect(expectedStatus)
-}
-
-function getVideoChangeOwnershipList (url: string, token: string) {
-  const path = '/api/v1/videos/ownership'
-
-  return request(url)
-    .get(path)
-    .query({ sort: '-createdAt' })
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function acceptChangeOwnership (
-  url: string,
-  token: string,
-  ownershipId: string,
-  channelId: number,
-  expectedStatus = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
-
-  return request(url)
-    .post(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .send({ channelId })
-    .expect(expectedStatus)
-}
-
-function refuseChangeOwnership (
-  url: string,
-  token: string,
-  ownershipId: string,
-  expectedStatus = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
-
-  return request(url)
-    .post(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(expectedStatus)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  changeVideoOwnership,
-  getVideoChangeOwnershipList,
-  acceptChangeOwnership,
-  refuseChangeOwnership
-}
diff --git a/shared/extra-utils/videos/video-channels.ts b/shared/extra-utils/videos/video-channels.ts
deleted file mode 100644 (file)
index 0aab93e..0000000
+++ /dev/null
@@ -1,192 +0,0 @@
-/* eslint-disable @typescript-eslint/no-floating-promises */
-
-import * as request from 'supertest'
-import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
-import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
-import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests'
-import { ServerInfo } from '../server/servers'
-import { MyUser, User } from '../../models/users/user.model'
-import { getMyUserInformation } from '../users/users'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getVideoChannelsList (url: string, start: number, count: number, sort?: string, withStats?: boolean) {
-  const path = '/api/v1/video-channels'
-
-  const req = request(url)
-    .get(path)
-    .query({ start: start })
-    .query({ count: count })
-
-  if (sort) req.query({ sort })
-  if (withStats) req.query({ withStats })
-
-  return req.set('Accept', 'application/json')
-            .expect(HttpStatusCode.OK_200)
-            .expect('Content-Type', /json/)
-}
-
-function getAccountVideoChannelsList (parameters: {
-  url: string
-  accountName: string
-  start?: number
-  count?: number
-  sort?: string
-  specialStatus?: HttpStatusCode
-  withStats?: boolean
-  search?: string
-}) {
-  const {
-    url,
-    accountName,
-    start,
-    count,
-    sort = 'createdAt',
-    specialStatus = HttpStatusCode.OK_200,
-    withStats = false,
-    search
-  } = parameters
-
-  const path = '/api/v1/accounts/' + accountName + '/video-channels'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: {
-      start,
-      count,
-      sort,
-      withStats,
-      search
-    },
-    statusCodeExpected: specialStatus
-  })
-}
-
-function addVideoChannel (
-  url: string,
-  token: string,
-  videoChannelAttributesArg: VideoChannelCreate,
-  expectedStatus = HttpStatusCode.OK_200
-) {
-  const path = '/api/v1/video-channels/'
-
-  // Default attributes
-  let attributes = {
-    displayName: 'my super video channel',
-    description: 'my super channel description',
-    support: 'my super channel support'
-  }
-  attributes = Object.assign(attributes, videoChannelAttributesArg)
-
-  return request(url)
-    .post(path)
-    .send(attributes)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(expectedStatus)
-}
-
-function updateVideoChannel (
-  url: string,
-  token: string,
-  channelName: string,
-  attributes: VideoChannelUpdate,
-  expectedStatus = HttpStatusCode.NO_CONTENT_204
-) {
-  const body: any = {}
-  const path = '/api/v1/video-channels/' + channelName
-
-  if (attributes.displayName) body.displayName = attributes.displayName
-  if (attributes.description) body.description = attributes.description
-  if (attributes.support) body.support = attributes.support
-  if (attributes.bulkVideosSupportUpdate) body.bulkVideosSupportUpdate = attributes.bulkVideosSupportUpdate
-
-  return request(url)
-    .put(path)
-    .send(body)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(expectedStatus)
-}
-
-function deleteVideoChannel (url: string, token: string, channelName: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/video-channels/' + channelName
-
-  return request(url)
-    .delete(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(expectedStatus)
-}
-
-function getVideoChannel (url: string, channelName: string) {
-  const path = '/api/v1/video-channels/' + channelName
-
-  return request(url)
-    .get(path)
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function updateVideoChannelImage (options: {
-  url: string
-  accessToken: string
-  fixture: string
-  videoChannelName: string | number
-  type: 'avatar' | 'banner'
-}) {
-  const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}/pick`
-
-  return updateImageRequest({ ...options, path, fieldname: options.type + 'file' })
-}
-
-function deleteVideoChannelImage (options: {
-  url: string
-  accessToken: string
-  videoChannelName: string | number
-  type: 'avatar' | 'banner'
-}) {
-  const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}`
-
-  return makeDeleteRequest({
-    url: options.url,
-    token: options.accessToken,
-    path,
-    statusCodeExpected: 204
-  })
-}
-
-function setDefaultVideoChannel (servers: ServerInfo[]) {
-  const tasks: Promise<any>[] = []
-
-  for (const server of servers) {
-    const p = getMyUserInformation(server.url, server.accessToken)
-      .then(res => { server.videoChannel = (res.body as User).videoChannels[0] })
-
-    tasks.push(p)
-  }
-
-  return Promise.all(tasks)
-}
-
-async function getDefaultVideoChannel (url: string, token: string) {
-  const res = await getMyUserInformation(url, token)
-
-  return (res.body as MyUser).videoChannels[0].id
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  updateVideoChannelImage,
-  getVideoChannelsList,
-  getAccountVideoChannelsList,
-  addVideoChannel,
-  updateVideoChannel,
-  deleteVideoChannel,
-  getVideoChannel,
-  setDefaultVideoChannel,
-  deleteVideoChannelImage,
-  getDefaultVideoChannel
-}
diff --git a/shared/extra-utils/videos/video-comments.ts b/shared/extra-utils/videos/video-comments.ts
deleted file mode 100644 (file)
index 71b9f87..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-/* eslint-disable @typescript-eslint/no-floating-promises */
-
-import * as request from 'supertest'
-import { makeDeleteRequest, makeGetRequest } from '../requests/requests'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getAdminVideoComments (options: {
-  url: string
-  token: string
-  start: number
-  count: number
-  sort?: string
-  isLocal?: boolean
-  search?: string
-  searchAccount?: string
-  searchVideo?: string
-}) {
-  const { url, token, start, count, sort, isLocal, search, searchAccount, searchVideo } = options
-  const path = '/api/v1/videos/comments'
-
-  const query = {
-    start,
-    count,
-    sort: sort || '-createdAt'
-  }
-
-  if (isLocal !== undefined) Object.assign(query, { isLocal })
-  if (search !== undefined) Object.assign(query, { search })
-  if (searchAccount !== undefined) Object.assign(query, { searchAccount })
-  if (searchVideo !== undefined) Object.assign(query, { searchVideo })
-
-  return makeGetRequest({
-    url,
-    path,
-    token,
-    query,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: string) {
-  const path = '/api/v1/videos/' + videoId + '/comment-threads'
-
-  const req = request(url)
-    .get(path)
-    .query({ start: start })
-    .query({ count: count })
-
-  if (sort) req.query({ sort })
-  if (token) req.set('Authorization', 'Bearer ' + token)
-
-  return req.set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) {
-  const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
-
-  const req = request(url)
-    .get(path)
-    .set('Accept', 'application/json')
-
-  if (token) req.set('Authorization', 'Bearer ' + token)
-
-  return req.expect(HttpStatusCode.OK_200)
-            .expect('Content-Type', /json/)
-}
-
-function addVideoCommentThread (
-  url: string,
-  token: string,
-  videoId: number | string,
-  text: string,
-  expectedStatus = HttpStatusCode.OK_200
-) {
-  const path = '/api/v1/videos/' + videoId + '/comment-threads'
-
-  return request(url)
-    .post(path)
-    .send({ text })
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(expectedStatus)
-}
-
-function addVideoCommentReply (
-  url: string,
-  token: string,
-  videoId: number | string,
-  inReplyToCommentId: number,
-  text: string,
-  expectedStatus = HttpStatusCode.OK_200
-) {
-  const path = '/api/v1/videos/' + videoId + '/comments/' + inReplyToCommentId
-
-  return request(url)
-    .post(path)
-    .send({ text })
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(expectedStatus)
-}
-
-async function findCommentId (url: string, videoId: number | string, text: string) {
-  const res = await getVideoCommentThreads(url, videoId, 0, 25, '-createdAt')
-
-  return res.body.data.find(c => c.text === text).id as number
-}
-
-function deleteVideoComment (
-  url: string,
-  token: string,
-  videoId: number | string,
-  commentId: number,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
-
-  return makeDeleteRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getVideoCommentThreads,
-  getAdminVideoComments,
-  getVideoThreadComments,
-  addVideoCommentThread,
-  addVideoCommentReply,
-  findCommentId,
-  deleteVideoComment
-}
diff --git a/shared/extra-utils/videos/video-history.ts b/shared/extra-utils/videos/video-history.ts
deleted file mode 100644 (file)
index b989e14..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function userWatchVideo (
-  url: string,
-  token: string,
-  videoId: number | string,
-  currentTime: number,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/videos/' + videoId + '/watching'
-  const fields = { currentTime }
-
-  return makePutBodyRequest({ url, path, token, fields, statusCodeExpected })
-}
-
-function listMyVideosHistory (url: string, token: string, search?: string) {
-  const path = '/api/v1/users/me/history/videos'
-
-  return makeGetRequest({
-    url,
-    path,
-    token,
-    query: {
-      search
-    },
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function removeMyVideosHistory (url: string, token: string, beforeDate?: string) {
-  const path = '/api/v1/users/me/history/videos/remove'
-
-  return makePostBodyRequest({
-    url,
-    path,
-    token,
-    fields: beforeDate ? { beforeDate } : {},
-    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  userWatchVideo,
-  listMyVideosHistory,
-  removeMyVideosHistory
-}
diff --git a/shared/extra-utils/videos/video-imports.ts b/shared/extra-utils/videos/video-imports.ts
deleted file mode 100644 (file)
index 81c0163..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-
-import { VideoImportCreate } from '../../models/videos'
-import { makeGetRequest, makeUploadRequest } from '../requests/requests'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getYoutubeVideoUrl () {
-  return 'https://www.youtube.com/watch?v=msX3jv1XdvM'
-}
-
-function getYoutubeHDRVideoUrl () {
-  /**
-   * The video is used to check format-selection correctness wrt. HDR,
-   * which brings its own set of oddities outside of a MediaSource.
-   * FIXME: refactor once HDR is supported at playback
-   *
-   * The video needs to have the following format_ids:
-   * (which you can check by using `youtube-dl <url> -F`):
-   * - 303 (1080p webm vp9)
-   * - 299 (1080p mp4 avc1)
-   * - 335 (1080p webm vp9.2 HDR)
-   *
-   * 15 jan. 2021: TEST VIDEO NOT CURRENTLY PROVIDING
-   * - 400 (1080p mp4 av01)
-   * - 315 (2160p webm vp9 HDR)
-   * - 337 (2160p webm vp9.2 HDR)
-   * - 401 (2160p mp4 av01 HDR)
-   */
-  return 'https://www.youtube.com/watch?v=qR5vOXbZsI4'
-}
-
-function getMagnetURI () {
-  // eslint-disable-next-line max-len
-  return 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4'
-}
-
-function getBadVideoUrl () {
-  return 'https://download.cpy.re/peertube/bad_video.mp4'
-}
-
-function getGoodVideoUrl () {
-  return 'https://download.cpy.re/peertube/good_video.mp4'
-}
-
-function importVideo (
-  url: string,
-  token: string,
-  attributes: VideoImportCreate & { torrentfile?: string },
-  statusCodeExpected = HttpStatusCode.OK_200
-) {
-  const path = '/api/v1/videos/imports'
-
-  let attaches: any = {}
-  if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
-
-  return makeUploadRequest({
-    url,
-    path,
-    token,
-    attaches,
-    fields: attributes,
-    statusCodeExpected
-  })
-}
-
-function getMyVideoImports (url: string, token: string, sort?: string) {
-  const path = '/api/v1/users/me/videos/imports'
-
-  const query = {}
-  if (sort) query['sort'] = sort
-
-  return makeGetRequest({
-    url,
-    query,
-    path,
-    token,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getBadVideoUrl,
-  getYoutubeVideoUrl,
-  getYoutubeHDRVideoUrl,
-  importVideo,
-  getMagnetURI,
-  getMyVideoImports,
-  getGoodVideoUrl
-}
diff --git a/shared/extra-utils/videos/video-playlists.ts b/shared/extra-utils/videos/video-playlists.ts
deleted file mode 100644 (file)
index c6f799e..0000000
+++ /dev/null
@@ -1,320 +0,0 @@
-import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
-import { VideoPlaylistCreate } from '../../models/videos/playlist/video-playlist-create.model'
-import { omit } from 'lodash'
-import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model'
-import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model'
-import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model'
-import { videoUUIDToId } from './videos'
-import { join } from 'path'
-import { root } from '..'
-import { readdir } from 'fs-extra'
-import { expect } from 'chai'
-import { VideoPlaylistType } from '../../models/videos/playlist/video-playlist-type.model'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getVideoPlaylistsList (url: string, start: number, count: number, sort?: string) {
-  const path = '/api/v1/video-playlists'
-
-  const query = {
-    start,
-    count,
-    sort
-  }
-
-  return makeGetRequest({
-    url,
-    path,
-    query,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoChannelPlaylistsList (url: string, videoChannelName: string, start: number, count: number, sort?: string) {
-  const path = '/api/v1/video-channels/' + videoChannelName + '/video-playlists'
-
-  const query = {
-    start,
-    count,
-    sort
-  }
-
-  return makeGetRequest({
-    url,
-    path,
-    query,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getAccountPlaylistsList (url: string, accountName: string, start: number, count: number, sort?: string, search?: string) {
-  const path = '/api/v1/accounts/' + accountName + '/video-playlists'
-
-  const query = {
-    start,
-    count,
-    sort,
-    search
-  }
-
-  return makeGetRequest({
-    url,
-    path,
-    query,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getAccountPlaylistsListWithToken (
-  url: string,
-  token: string,
-  accountName: string,
-  start: number,
-  count: number,
-  playlistType?: VideoPlaylistType,
-  sort?: string
-) {
-  const path = '/api/v1/accounts/' + accountName + '/video-playlists'
-
-  const query = {
-    start,
-    count,
-    playlistType,
-    sort
-  }
-
-  return makeGetRequest({
-    url,
-    token,
-    path,
-    query,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoPlaylist (url: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/video-playlists/' + playlistId
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected
-  })
-}
-
-function getVideoPlaylistWithToken (url: string, token: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
-  const path = '/api/v1/video-playlists/' + playlistId
-
-  return makeGetRequest({
-    url,
-    token,
-    path,
-    statusCodeExpected
-  })
-}
-
-function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/video-playlists/' + playlistId
-
-  return makeDeleteRequest({
-    url,
-    path,
-    token,
-    statusCodeExpected
-  })
-}
-
-function createVideoPlaylist (options: {
-  url: string
-  token: string
-  playlistAttrs: VideoPlaylistCreate
-  expectedStatus?: number
-}) {
-  const path = '/api/v1/video-playlists'
-
-  const fields = omit(options.playlistAttrs, 'thumbnailfile')
-
-  const attaches = options.playlistAttrs.thumbnailfile
-    ? { thumbnailfile: options.playlistAttrs.thumbnailfile }
-    : {}
-
-  return makeUploadRequest({
-    method: 'POST',
-    url: options.url,
-    path,
-    token: options.token,
-    fields,
-    attaches,
-    statusCodeExpected: options.expectedStatus || HttpStatusCode.OK_200
-  })
-}
-
-function updateVideoPlaylist (options: {
-  url: string
-  token: string
-  playlistAttrs: VideoPlaylistUpdate
-  playlistId: number | string
-  expectedStatus?: number
-}) {
-  const path = '/api/v1/video-playlists/' + options.playlistId
-
-  const fields = omit(options.playlistAttrs, 'thumbnailfile')
-
-  const attaches = options.playlistAttrs.thumbnailfile
-    ? { thumbnailfile: options.playlistAttrs.thumbnailfile }
-    : {}
-
-  return makeUploadRequest({
-    method: 'PUT',
-    url: options.url,
-    path,
-    token: options.token,
-    fields,
-    attaches,
-    statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-async function addVideoInPlaylist (options: {
-  url: string
-  token: string
-  playlistId: number | string
-  elementAttrs: VideoPlaylistElementCreate | { videoId: string }
-  expectedStatus?: number
-}) {
-  options.elementAttrs.videoId = await videoUUIDToId(options.url, options.elementAttrs.videoId)
-
-  const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
-
-  return makePostBodyRequest({
-    url: options.url,
-    path,
-    token: options.token,
-    fields: options.elementAttrs,
-    statusCodeExpected: options.expectedStatus || HttpStatusCode.OK_200
-  })
-}
-
-function updateVideoPlaylistElement (options: {
-  url: string
-  token: string
-  playlistId: number | string
-  playlistElementId: number | string
-  elementAttrs: VideoPlaylistElementUpdate
-  expectedStatus?: number
-}) {
-  const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId
-
-  return makePutBodyRequest({
-    url: options.url,
-    path,
-    token: options.token,
-    fields: options.elementAttrs,
-    statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-function removeVideoFromPlaylist (options: {
-  url: string
-  token: string
-  playlistId: number | string
-  playlistElementId: number
-  expectedStatus?: number
-}) {
-  const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId
-
-  return makeDeleteRequest({
-    url: options.url,
-    path,
-    token: options.token,
-    statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-function reorderVideosPlaylist (options: {
-  url: string
-  token: string
-  playlistId: number | string
-  elementAttrs: {
-    startPosition: number
-    insertAfterPosition: number
-    reorderLength?: number
-  }
-  expectedStatus?: number
-}) {
-  const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
-
-  return makePostBodyRequest({
-    url: options.url,
-    path,
-    token: options.token,
-    fields: options.elementAttrs,
-    statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
-  })
-}
-
-async function checkPlaylistFilesWereRemoved (
-  playlistUUID: string,
-  internalServerNumber: number,
-  directories = [ 'thumbnails' ]
-) {
-  const testDirectory = 'test' + internalServerNumber
-
-  for (const directory of directories) {
-    const directoryPath = join(root(), testDirectory, directory)
-
-    const files = await readdir(directoryPath)
-    for (const file of files) {
-      expect(file).to.not.contain(playlistUUID)
-    }
-  }
-}
-
-function getVideoPlaylistPrivacies (url: string) {
-  const path = '/api/v1/video-playlists/privacies'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function doVideosExistInMyPlaylist (url: string, token: string, videoIds: number[]) {
-  const path = '/api/v1/users/me/video-playlists/videos-exist'
-
-  return makeGetRequest({
-    url,
-    token,
-    path,
-    query: { videoIds },
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getVideoPlaylistPrivacies,
-
-  getVideoPlaylistsList,
-  getVideoChannelPlaylistsList,
-  getAccountPlaylistsList,
-  getAccountPlaylistsListWithToken,
-
-  getVideoPlaylist,
-  getVideoPlaylistWithToken,
-
-  createVideoPlaylist,
-  updateVideoPlaylist,
-  deleteVideoPlaylist,
-
-  addVideoInPlaylist,
-  updateVideoPlaylistElement,
-  removeVideoFromPlaylist,
-
-  reorderVideosPlaylist,
-
-  checkPlaylistFilesWereRemoved,
-
-  doVideosExistInMyPlaylist
-}
diff --git a/shared/extra-utils/videos/video-streaming-playlists.ts b/shared/extra-utils/videos/video-streaming-playlists.ts
deleted file mode 100644 (file)
index 99c2e18..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-import { makeRawRequest } from '../requests/requests'
-import { sha256 } from '../../../server/helpers/core-utils'
-import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
-import { expect } from 'chai'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getPlaylist (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  return makeRawRequest(url, statusCodeExpected)
-}
-
-function getSegment (url: string, statusCodeExpected = HttpStatusCode.OK_200, range?: string) {
-  return makeRawRequest(url, statusCodeExpected, range)
-}
-
-function getSegmentSha256 (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  return makeRawRequest(url, statusCodeExpected)
-}
-
-async function checkSegmentHash (
-  baseUrlPlaylist: string,
-  baseUrlSegment: string,
-  videoUUID: string,
-  resolution: number,
-  hlsPlaylist: VideoStreamingPlaylist
-) {
-  const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
-  const playlist = res.text
-
-  const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
-
-  const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
-
-  const length = parseInt(matches[1], 10)
-  const offset = parseInt(matches[2], 10)
-  const range = `${offset}-${offset + length - 1}`
-
-  const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, HttpStatusCode.PARTIAL_CONTENT_206, `bytes=${range}`)
-
-  const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
-
-  const sha256Server = resSha.body[videoName][range]
-  expect(sha256(res2.body)).to.equal(sha256Server)
-}
-
-async function checkLiveSegmentHash (
-  baseUrlSegment: string,
-  videoUUID: string,
-  segmentName: string,
-  hlsPlaylist: VideoStreamingPlaylist
-) {
-  const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${segmentName}`)
-
-  const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
-
-  const sha256Server = resSha.body[segmentName]
-  expect(sha256(res2.body)).to.equal(sha256Server)
-}
-
-async function checkResolutionsInMasterPlaylist (playlistUrl: string, resolutions: number[]) {
-  const res = await getPlaylist(playlistUrl)
-
-  const masterPlaylist = res.text
-
-  for (const resolution of resolutions) {
-    const reg = new RegExp(
-      '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
-    )
-
-    expect(masterPlaylist).to.match(reg)
-  }
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getPlaylist,
-  getSegment,
-  checkResolutionsInMasterPlaylist,
-  getSegmentSha256,
-  checkLiveSegmentHash,
-  checkSegmentHash
-}
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts
new file mode 100644 (file)
index 0000000..33725bf
--- /dev/null
@@ -0,0 +1,599 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
+
+import { expect } from 'chai'
+import { createReadStream, stat } from 'fs-extra'
+import got, { Response as GotResponse } from 'got'
+import { omit } from 'lodash'
+import validator from 'validator'
+import { buildUUID } from '@server/helpers/uuid'
+import { loadLanguages } from '@server/initializers/constants'
+import { pick } from '@shared/core-utils'
+import {
+  HttpStatusCode,
+  ResultList,
+  UserVideoRateType,
+  Video,
+  VideoCreate,
+  VideoCreateResult,
+  VideoDetails,
+  VideoFileMetadata,
+  VideoPrivacy,
+  VideosCommonQuery,
+  VideosWithSearchCommonQuery
+} from '@shared/models'
+import { buildAbsoluteFixturePath, wait } from '../miscs'
+import { unwrapBody } from '../requests'
+import { PeerTubeServer, waitJobs } from '../server'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
+  fixture?: string
+  thumbnailfile?: string
+  previewfile?: string
+}
+
+export class VideosCommand extends AbstractCommand {
+
+  constructor (server: PeerTubeServer) {
+    super(server)
+
+    loadLanguages()
+  }
+
+  getCategories (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/videos/categories'
+
+    return this.getRequestBody<{ [id: number]: string }>({
+      ...options,
+      path,
+
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getLicences (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/videos/licences'
+
+    return this.getRequestBody<{ [id: number]: string }>({
+      ...options,
+      path,
+
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getLanguages (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/videos/languages'
+
+    return this.getRequestBody<{ [id: string]: string }>({
+      ...options,
+      path,
+
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getPrivacies (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/videos/privacies'
+
+    return this.getRequestBody<{ [id in VideoPrivacy]: string }>({
+      ...options,
+      path,
+
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  getDescription (options: OverrideCommandOptions & {
+    descriptionPath: string
+  }) {
+    return this.getRequestBody<{ description: string }>({
+      ...options,
+      path: options.descriptionPath,
+
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getFileMetadata (options: OverrideCommandOptions & {
+    url: string
+  }) {
+    return unwrapBody<VideoFileMetadata>(this.getRawRequest({
+      ...options,
+
+      url: options.url,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  // ---------------------------------------------------------------------------
+
+  view (options: OverrideCommandOptions & {
+    id: number | string
+    xForwardedFor?: string
+  }) {
+    const { id, xForwardedFor } = options
+    const path = '/api/v1/videos/' + id + '/views'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      xForwardedFor,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  rate (options: OverrideCommandOptions & {
+    id: number | string
+    rating: UserVideoRateType
+  }) {
+    const { id, rating } = options
+    const path = '/api/v1/videos/' + id + '/rate'
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: { rating },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  get (options: OverrideCommandOptions & {
+    id: number | string
+  }) {
+    const path = '/api/v1/videos/' + options.id
+
+    return this.getRequestBody<VideoDetails>({
+      ...options,
+
+      path,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  getWithToken (options: OverrideCommandOptions & {
+    id: number | string
+  }) {
+    return this.get({
+      ...options,
+
+      token: this.buildCommonRequestToken({ ...options, implicitToken: true })
+    })
+  }
+
+  async getId (options: OverrideCommandOptions & {
+    uuid: number | string
+  }) {
+    const { uuid } = options
+
+    if (validator.isUUID('' + uuid) === false) return uuid as number
+
+    const { id } = await this.get({ ...options, id: uuid })
+
+    return id
+  }
+
+  // ---------------------------------------------------------------------------
+
+  listMyVideos (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+    search?: string
+    isLive?: boolean
+  } = {}) {
+    const path = '/api/v1/users/me/videos'
+
+    return this.getRequestBody<ResultList<Video>>({
+      ...options,
+
+      path,
+      query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]),
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
+    const path = '/api/v1/videos'
+
+    const query = this.buildListQuery(options)
+
+    return this.getRequestBody<ResultList<Video>>({
+      ...options,
+
+      path,
+      query: { sort: 'name', ...query },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
+    return this.list({
+      ...options,
+
+      token: this.buildCommonRequestToken({ ...options, implicitToken: true })
+    })
+  }
+
+  listByAccount (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
+    handle: string
+  }) {
+    const { handle, search } = options
+    const path = '/api/v1/accounts/' + handle + '/videos'
+
+    return this.getRequestBody<ResultList<Video>>({
+      ...options,
+
+      path,
+      query: { search, ...this.buildListQuery(options) },
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
+    handle: string
+  }) {
+    const { handle } = options
+    const path = '/api/v1/video-channels/' + handle + '/videos'
+
+    return this.getRequestBody<ResultList<Video>>({
+      ...options,
+
+      path,
+      query: this.buildListQuery(options),
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  async find (options: OverrideCommandOptions & {
+    name: string
+  }) {
+    const { data } = await this.list(options)
+
+    return data.find(v => v.name === options.name)
+  }
+
+  // ---------------------------------------------------------------------------
+
+  update (options: OverrideCommandOptions & {
+    id: number | string
+    attributes?: VideoEdit
+  }) {
+    const { id, attributes = {} } = options
+    const path = '/api/v1/videos/' + id
+
+    // Upload request
+    if (attributes.thumbnailfile || attributes.previewfile) {
+      const attaches: any = {}
+      if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
+      if (attributes.previewfile) attaches.previewfile = attributes.previewfile
+
+      return this.putUploadRequest({
+        ...options,
+
+        path,
+        fields: options.attributes,
+        attaches: {
+          thumbnailfile: attributes.thumbnailfile,
+          previewfile: attributes.previewfile
+        },
+        implicitToken: true,
+        defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+      })
+    }
+
+    return this.putBodyRequest({
+      ...options,
+
+      path,
+      fields: options.attributes,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  remove (options: OverrideCommandOptions & {
+    id: number | string
+  }) {
+    const path = '/api/v1/videos/' + options.id
+
+    return unwrapBody(this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    }))
+  }
+
+  async removeAll () {
+    const { data } = await this.list()
+
+    for (const v of data) {
+      await this.remove({ id: v.id })
+    }
+  }
+
+  // ---------------------------------------------------------------------------
+
+  async upload (options: OverrideCommandOptions & {
+    attributes?: VideoEdit
+    mode?: 'legacy' | 'resumable' // default legacy
+  } = {}) {
+    const { mode = 'legacy' } = options
+    let defaultChannelId = 1
+
+    try {
+      const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
+      defaultChannelId = videoChannels[0].id
+    } catch (e) { /* empty */ }
+
+    // Override default attributes
+    const attributes = {
+      name: 'my super video',
+      category: 5,
+      licence: 4,
+      language: 'zh',
+      channelId: defaultChannelId,
+      nsfw: true,
+      waitTranscoding: false,
+      description: 'my super description',
+      support: 'my super support text',
+      tags: [ 'tag' ],
+      privacy: VideoPrivacy.PUBLIC,
+      commentsEnabled: true,
+      downloadEnabled: true,
+      fixture: 'video_short.webm',
+
+      ...options.attributes
+    }
+
+    const created = mode === 'legacy'
+      ? await this.buildLegacyUpload({ ...options, attributes })
+      : await this.buildResumeUpload({ ...options, attributes })
+
+    // Wait torrent generation
+    const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
+    if (expectedStatus === HttpStatusCode.OK_200) {
+      let video: VideoDetails
+
+      do {
+        video = await this.getWithToken({ ...options, id: created.uuid })
+
+        await wait(50)
+      } while (!video.files[0].torrentUrl)
+    }
+
+    return created
+  }
+
+  async buildLegacyUpload (options: OverrideCommandOptions & {
+    attributes: VideoEdit
+  }): Promise<VideoCreateResult> {
+    const path = '/api/v1/videos/upload'
+
+    return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
+      ...options,
+
+      path,
+      fields: this.buildUploadFields(options.attributes),
+      attaches: this.buildUploadAttaches(options.attributes),
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })).then(body => body.video || body as any)
+  }
+
+  async buildResumeUpload (options: OverrideCommandOptions & {
+    attributes: VideoEdit
+  }): Promise<VideoCreateResult> {
+    const { attributes, expectedStatus } = options
+
+    let size = 0
+    let videoFilePath: string
+    let mimetype = 'video/mp4'
+
+    if (attributes.fixture) {
+      videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
+      size = (await stat(videoFilePath)).size
+
+      if (videoFilePath.endsWith('.mkv')) {
+        mimetype = 'video/x-matroska'
+      } else if (videoFilePath.endsWith('.webm')) {
+        mimetype = 'video/webm'
+      }
+    }
+
+    // Do not check status automatically, we'll check it manually
+    const initializeSessionRes = await this.prepareResumableUpload({ ...options, expectedStatus: null, attributes, size, mimetype })
+    const initStatus = initializeSessionRes.status
+
+    if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
+      const locationHeader = initializeSessionRes.header['location']
+      expect(locationHeader).to.not.be.undefined
+
+      const pathUploadId = locationHeader.split('?')[1]
+
+      const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
+
+      return result.body?.video || result.body as any
+    }
+
+    const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
+      ? HttpStatusCode.CREATED_201
+      : expectedStatus
+
+    expect(initStatus).to.equal(expectedInitStatus)
+
+    return initializeSessionRes.body.video || initializeSessionRes.body
+  }
+
+  async prepareResumableUpload (options: OverrideCommandOptions & {
+    attributes: VideoEdit
+    size: number
+    mimetype: string
+  }) {
+    const { attributes, size, mimetype } = options
+
+    const path = '/api/v1/videos/upload-resumable'
+
+    return this.postUploadRequest({
+      ...options,
+
+      path,
+      headers: {
+        'X-Upload-Content-Type': mimetype,
+        'X-Upload-Content-Length': size.toString()
+      },
+      fields: { filename: attributes.fixture, ...this.buildUploadFields(options.attributes) },
+      // Fixture will be sent later
+      attaches: this.buildUploadAttaches(omit(options.attributes, 'fixture')),
+      implicitToken: true,
+
+      defaultExpectedStatus: null
+    })
+  }
+
+  sendResumableChunks (options: OverrideCommandOptions & {
+    pathUploadId: string
+    videoFilePath: string
+    size: number
+    contentLength?: number
+    contentRangeBuilder?: (start: number, chunk: any) => string
+  }) {
+    const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options
+
+    const path = '/api/v1/videos/upload-resumable'
+    let start = 0
+
+    const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
+    const url = this.server.url
+
+    const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
+    return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
+      readable.on('data', async function onData (chunk) {
+        readable.pause()
+
+        const headers = {
+          'Authorization': 'Bearer ' + token,
+          'Content-Type': 'application/octet-stream',
+          'Content-Range': contentRangeBuilder
+            ? contentRangeBuilder(start, chunk)
+            : `bytes ${start}-${start + chunk.length - 1}/${size}`,
+          'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
+        }
+
+        const res = await got<{ video: VideoCreateResult }>({
+          url,
+          method: 'put',
+          headers,
+          path: path + '?' + pathUploadId,
+          body: chunk,
+          responseType: 'json',
+          throwHttpErrors: false
+        })
+
+        start += chunk.length
+
+        if (res.statusCode === expectedStatus) {
+          return resolve(res)
+        }
+
+        if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
+          readable.off('data', onData)
+          return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
+        }
+
+        readable.resume()
+      })
+    })
+  }
+
+  quickUpload (options: OverrideCommandOptions & {
+    name: string
+    nsfw?: boolean
+    privacy?: VideoPrivacy
+    fixture?: string
+  }) {
+    const attributes: VideoEdit = { name: options.name }
+    if (options.nsfw) attributes.nsfw = options.nsfw
+    if (options.privacy) attributes.privacy = options.privacy
+    if (options.fixture) attributes.fixture = options.fixture
+
+    return this.upload({ ...options, attributes })
+  }
+
+  async randomUpload (options: OverrideCommandOptions & {
+    wait?: boolean // default true
+    additionalParams?: VideoEdit & { prefixName?: string }
+  } = {}) {
+    const { wait = true, additionalParams } = options
+    const prefixName = additionalParams?.prefixName || ''
+    const name = prefixName + buildUUID()
+
+    const attributes = { name, ...additionalParams }
+
+    const result = await this.upload({ ...options, attributes })
+
+    if (wait) await waitJobs([ this.server ])
+
+    return { ...result, name }
+  }
+
+  // ---------------------------------------------------------------------------
+
+  private buildListQuery (options: VideosCommonQuery) {
+    return pick(options, [
+      'start',
+      'count',
+      'sort',
+      'nsfw',
+      'isLive',
+      'categoryOneOf',
+      'licenceOneOf',
+      'languageOneOf',
+      'tagsOneOf',
+      'tagsAllOf',
+      'filter',
+      'skipCount'
+    ])
+  }
+
+  private buildUploadFields (attributes: VideoEdit) {
+    return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
+  }
+
+  private buildUploadAttaches (attributes: VideoEdit) {
+    const attaches: { [ name: string ]: string } = {}
+
+    for (const key of [ 'thumbnailfile', 'previewfile' ]) {
+      if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
+    }
+
+    if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
+
+    return attaches
+  }
+}
index 469ea4d638cf9f3014114ade4f0b545b69674565..a1d2ba0fc14fdde45a37da288399ed751dbf8dd4 100644 (file)
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
 
 import { expect } from 'chai'
-import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra'
-import got, { Response as GotResponse } from 'got/dist/source'
-import * as parseTorrent from 'parse-torrent'
-import { join } from 'path'
-import * as request from 'supertest'
-import validator from 'validator'
+import { pathExists, readdir } from 'fs-extra'
+import { basename, join } from 'path'
 import { getLowercaseExtension } from '@server/helpers/core-utils'
-import { buildUUID } from '@server/helpers/uuid'
-import { HttpStatusCode } from '@shared/core-utils'
-import { VideosCommonQuery } from '@shared/models'
-import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
-import { VideoDetails, VideoPrivacy } from '../../models/videos'
-import {
-  buildAbsoluteFixturePath,
-  buildServerDirectory,
-  dateIsValid,
-  immutableAssign,
-  testImage,
-  wait,
-  webtorrentAdd
-} from '../miscs/miscs'
-import { makeGetRequest, makePutBodyRequest, makeRawRequest, makeUploadRequest } from '../requests/requests'
-import { waitJobs } from '../server/jobs'
-import { ServerInfo } from '../server/servers'
-import { getMyUserInformation } from '../users/users'
-
-loadLanguages()
-
-type VideoAttributes = {
-  name?: string
-  category?: number
-  licence?: number
-  language?: string
-  nsfw?: boolean
-  commentsEnabled?: boolean
-  downloadEnabled?: boolean
-  waitTranscoding?: boolean
-  description?: string
-  originallyPublishedAt?: string
-  tags?: string[]
-  channelId?: number
-  privacy?: VideoPrivacy
-  fixture?: string
-  support?: string
-  thumbnailfile?: string
-  previewfile?: string
-  scheduleUpdate?: {
-    updateAt: string
-    privacy?: VideoPrivacy
-  }
-}
-
-function getVideoCategories (url: string) {
-  const path = '/api/v1/videos/categories'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoLicences (url: string) {
-  const path = '/api/v1/videos/licences'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoLanguages (url: string) {
-  const path = '/api/v1/videos/languages'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoPrivacies (url: string) {
-  const path = '/api/v1/videos/privacies'
-
-  return makeGetRequest({
-    url,
-    path,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/videos/' + id
-
-  return request(url)
-          .get(path)
-          .set('Accept', 'application/json')
-          .expect(expectedStatus)
-}
+import { uuidRegex } from '@shared/core-utils'
+import { HttpStatusCode, VideoCaption, VideoDetails } from '@shared/models'
+import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
+import { dateIsValid, testImage, webtorrentAdd } from '../miscs'
+import { makeRawRequest } from '../requests/requests'
+import { waitJobs } from '../server'
+import { PeerTubeServer } from '../server/server'
+import { VideoEdit } from './videos-command'
+
+async function checkVideoFilesWereRemoved (options: {
+  server: PeerTubeServer
+  video: VideoDetails
+  captions?: VideoCaption[]
+  onlyVideoFiles?: boolean // default false
+}) {
+  const { video, server, captions = [], onlyVideoFiles = false } = options
 
-async function getVideoIdFromUUID (url: string, uuid: string) {
-  const res = await getVideo(url, uuid)
+  const webtorrentFiles = video.files || []
+  const hlsFiles = video.streamingPlaylists[0]?.files || []
 
-  return res.body.id
-}
+  const thumbnailName = basename(video.thumbnailPath)
+  const previewName = basename(video.previewPath)
 
-function getVideoFileMetadataUrl (url: string) {
-  return request(url)
-    .get('/')
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
+  const torrentNames = webtorrentFiles.concat(hlsFiles).map(f => basename(f.torrentUrl))
 
-function viewVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204, xForwardedFor?: string) {
-  const path = '/api/v1/videos/' + id + '/views'
+  const captionNames = captions.map(c => basename(c.captionPath))
 
-  const req = request(url)
-    .post(path)
-    .set('Accept', 'application/json')
+  const webtorrentFilenames = webtorrentFiles.map(f => basename(f.fileUrl))
+  const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl))
 
-  if (xForwardedFor) {
-    req.set('X-Forwarded-For', xForwardedFor)
+  let directories: { [ directory: string ]: string[] } = {
+    videos: webtorrentFilenames,
+    redundancy: webtorrentFilenames,
+    [join('playlists', 'hls')]: hlsFilenames,
+    [join('redundancy', 'hls')]: hlsFilenames
   }
 
-  return req.expect(expectedStatus)
-}
-
-function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/videos/' + id
-
-  return request(url)
-    .get(path)
-    .set('Authorization', 'Bearer ' + token)
-    .set('Accept', 'application/json')
-    .expect(expectedStatus)
-}
-
-function getVideoDescription (url: string, descriptionPath: string) {
-  return request(url)
-    .get(descriptionPath)
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getVideosList (url: string) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-          .get(path)
-          .query({ sort: 'name' })
-          .set('Accept', 'application/json')
-          .expect(HttpStatusCode.OK_200)
-          .expect('Content-Type', /json/)
-}
-
-function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-    .get(path)
-    .set('Authorization', 'Bearer ' + token)
-    .query(immutableAssign(query, { sort: 'name' }))
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getLocalVideos (url: string) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-    .get(path)
-    .query({ sort: 'name', filter: 'local' })
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string, search?: string) {
-  const path = '/api/v1/users/me/videos'
-
-  const req = request(url)
-    .get(path)
-    .query({ start: start })
-    .query({ count: count })
-    .query({ search: search })
-
-  if (sort) req.query({ sort })
-
-  return req.set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) {
-  const path = '/api/v1/users/me/videos'
-
-  return makeGetRequest({
-    url,
-    path,
-    token: accessToken,
-    query,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getAccountVideos (
-  url: string,
-  accessToken: string,
-  accountName: string,
-  start: number,
-  count: number,
-  sort?: string,
-  query: {
-    nsfw?: boolean
-    search?: string
-  } = {}
-) {
-  const path = '/api/v1/accounts/' + accountName + '/videos'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: immutableAssign(query, {
-      start,
-      count,
-      sort
-    }),
-    token: accessToken,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideoChannelVideos (
-  url: string,
-  accessToken: string,
-  videoChannelName: string,
-  start: number,
-  count: number,
-  sort?: string,
-  query: { nsfw?: boolean } = {}
-) {
-  const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: immutableAssign(query, {
-      start,
-      count,
-      sort
-    }),
-    token: accessToken,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getPlaylistVideos (
-  url: string,
-  accessToken: string,
-  playlistId: number | string,
-  start: number,
-  count: number,
-  query: { nsfw?: boolean } = {}
-) {
-  const path = '/api/v1/video-playlists/' + playlistId + '/videos'
-
-  return makeGetRequest({
-    url,
-    path,
-    query: immutableAssign(query, {
-      start,
-      count
-    }),
-    token: accessToken,
-    statusCodeExpected: HttpStatusCode.OK_200
-  })
-}
-
-function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
-  const path = '/api/v1/videos'
-
-  const req = request(url)
-              .get(path)
-              .query({ start: start })
-              .query({ count: count })
-
-  if (sort) req.query({ sort })
-  if (skipCount) req.query({ skipCount })
-
-  return req.set('Accept', 'application/json')
-           .expect(HttpStatusCode.OK_200)
-           .expect('Content-Type', /json/)
-}
-
-function getVideosListSort (url: string, sort: string) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-          .get(path)
-          .query({ sort: sort })
-          .set('Accept', 'application/json')
-          .expect(HttpStatusCode.OK_200)
-          .expect('Content-Type', /json/)
-}
-
-function getVideosWithFilters (url: string, query: VideosCommonQuery) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-    .get(path)
-    .query(query)
-    .set('Accept', 'application/json')
-    .expect(HttpStatusCode.OK_200)
-    .expect('Content-Type', /json/)
-}
-
-function removeVideo (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/videos'
-
-  return request(url)
-          .delete(path + '/' + id)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + token)
-          .expect(expectedStatus)
-}
+  if (onlyVideoFiles !== true) {
+    directories = {
+      ...directories,
 
-async function removeAllVideos (server: ServerInfo) {
-  const resVideos = await getVideosList(server.url)
-
-  for (const v of resVideos.body.data) {
-    await removeVideo(server.url, server.accessToken, v.id)
+      thumbnails: [ thumbnailName ],
+      previews: [ previewName ],
+      torrents: torrentNames,
+      captions: captionNames
+    }
   }
-}
 
-async function checkVideoFilesWereRemoved (
-  videoUUID: string,
-  serverNumber: number,
-  directories = [
-    'redundancy',
-    'videos',
-    'thumbnails',
-    'torrents',
-    'previews',
-    'captions',
-    join('playlists', 'hls'),
-    join('redundancy', 'hls')
-  ]
-) {
-  for (const directory of directories) {
-    const directoryPath = buildServerDirectory({ internalServerNumber: serverNumber }, directory)
+  for (const directory of Object.keys(directories)) {
+    const directoryPath = server.servers.buildDirectory(directory)
 
     const directoryExists = await pathExists(directoryPath)
     if (directoryExists === false) continue
 
-    const files = await readdir(directoryPath)
-    for (const file of files) {
-      expect(file, `File ${file} should not exist in ${directoryPath}`).to.not.contain(videoUUID)
+    const existingFiles = await readdir(directoryPath)
+    for (const existingFile of existingFiles) {
+      for (const shouldNotExist of directories[directory]) {
+        expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist)
+      }
     }
   }
 }
 
-async function uploadVideo (
-  url: string,
-  accessToken: string,
-  videoAttributesArg: VideoAttributes,
-  specialStatus = HttpStatusCode.OK_200,
-  mode: 'legacy' | 'resumable' = 'legacy'
-) {
-  let defaultChannelId = '1'
-
-  try {
-    const res = await getMyUserInformation(url, accessToken)
-    defaultChannelId = res.body.videoChannels[0].id
-  } catch (e) { /* empty */ }
-
-  // Override default attributes
-  const attributes = Object.assign({
-    name: 'my super video',
-    category: 5,
-    licence: 4,
-    language: 'zh',
-    channelId: defaultChannelId,
-    nsfw: true,
-    waitTranscoding: false,
-    description: 'my super description',
-    support: 'my super support text',
-    tags: [ 'tag' ],
-    privacy: VideoPrivacy.PUBLIC,
-    commentsEnabled: true,
-    downloadEnabled: true,
-    fixture: 'video_short.webm'
-  }, videoAttributesArg)
-
-  const res = mode === 'legacy'
-    ? await buildLegacyUpload(url, accessToken, attributes, specialStatus)
-    : await buildResumeUpload(url, accessToken, attributes, specialStatus)
-
-  // Wait torrent generation
-  if (specialStatus === HttpStatusCode.OK_200) {
-    let video: VideoDetails
-    do {
-      const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid)
-      video = resVideo.body
-
-      await wait(50)
-    } while (!video.files[0].torrentUrl)
+async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) {
+  for (const server of servers) {
+    server.store.videoDetails = await server.videos.get({ id: uuid })
   }
-
-  return res
 }
 
 function checkUploadVideoParam (
-  url: string,
+  server: PeerTubeServer,
   token: string,
-  attributes: Partial<VideoAttributes>,
-  specialStatus = HttpStatusCode.OK_200,
+  attributes: Partial<VideoEdit>,
+  expectedStatus = HttpStatusCode.OK_200,
   mode: 'legacy' | 'resumable' = 'legacy'
 ) {
   return mode === 'legacy'
-    ? buildLegacyUpload(url, token, attributes, specialStatus)
-    : buildResumeUpload(url, token, attributes, specialStatus)
-}
-
-async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
-  const path = '/api/v1/videos/upload'
-  const req = request(url)
-              .post(path)
-              .set('Accept', 'application/json')
-              .set('Authorization', 'Bearer ' + token)
-
-  buildUploadReq(req, attributes)
-
-  if (attributes.fixture !== undefined) {
-    req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
-  }
-
-  return req.expect(specialStatus)
-}
-
-async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
-  let size = 0
-  let videoFilePath: string
-  let mimetype = 'video/mp4'
-
-  if (attributes.fixture) {
-    videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
-    size = (await stat(videoFilePath)).size
-
-    if (videoFilePath.endsWith('.mkv')) {
-      mimetype = 'video/x-matroska'
-    } else if (videoFilePath.endsWith('.webm')) {
-      mimetype = 'video/webm'
-    }
-  }
-
-  const initializeSessionRes = await prepareResumableUpload({ url, token, attributes, size, mimetype })
-  const initStatus = initializeSessionRes.status
-
-  if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
-    const locationHeader = initializeSessionRes.header['location']
-    expect(locationHeader).to.not.be.undefined
-
-    const pathUploadId = locationHeader.split('?')[1]
-
-    return sendResumableChunks({ url, token, pathUploadId, videoFilePath, size, specialStatus })
-  }
-
-  const expectedInitStatus = specialStatus === HttpStatusCode.OK_200
-    ? HttpStatusCode.CREATED_201
-    : specialStatus
-
-  expect(initStatus).to.equal(expectedInitStatus)
-
-  return initializeSessionRes
-}
-
-async function prepareResumableUpload (options: {
-  url: string
-  token: string
-  attributes: VideoAttributes
-  size: number
-  mimetype: string
-}) {
-  const { url, token, attributes, size, mimetype } = options
-
-  const path = '/api/v1/videos/upload-resumable'
-
-  const req = request(url)
-              .post(path)
-              .set('Authorization', 'Bearer ' + token)
-              .set('X-Upload-Content-Type', mimetype)
-              .set('X-Upload-Content-Length', size.toString())
-
-  buildUploadReq(req, attributes)
-
-  if (attributes.fixture) {
-    req.field('filename', attributes.fixture)
-  }
-
-  return req
-}
-
-function sendResumableChunks (options: {
-  url: string
-  token: string
-  pathUploadId: string
-  videoFilePath: string
-  size: number
-  specialStatus?: HttpStatusCode
-  contentLength?: number
-  contentRangeBuilder?: (start: number, chunk: any) => string
-}) {
-  const { url, token, pathUploadId, videoFilePath, size, specialStatus, contentLength, contentRangeBuilder } = options
-
-  const expectedStatus = specialStatus || HttpStatusCode.OK_200
-
-  const path = '/api/v1/videos/upload-resumable'
-  let start = 0
-
-  const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
-  return new Promise<GotResponse>((resolve, reject) => {
-    readable.on('data', async function onData (chunk) {
-      readable.pause()
-
-      const headers = {
-        'Authorization': 'Bearer ' + token,
-        'Content-Type': 'application/octet-stream',
-        'Content-Range': contentRangeBuilder
-          ? contentRangeBuilder(start, chunk)
-          : `bytes ${start}-${start + chunk.length - 1}/${size}`,
-        'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
-      }
-
-      const res = await got({
-        url,
-        method: 'put',
-        headers,
-        path: path + '?' + pathUploadId,
-        body: chunk,
-        responseType: 'json',
-        throwHttpErrors: false
-      })
-
-      start += chunk.length
-
-      if (res.statusCode === expectedStatus) {
-        return resolve(res)
-      }
-
-      if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
-        readable.off('data', onData)
-        return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
-      }
-
-      readable.resume()
-    })
-  })
-}
-
-function updateVideo (
-  url: string,
-  accessToken: string,
-  id: number | string,
-  attributes: VideoAttributes,
-  statusCodeExpected = HttpStatusCode.NO_CONTENT_204
-) {
-  const path = '/api/v1/videos/' + id
-  const body = {}
-
-  if (attributes.name) body['name'] = attributes.name
-  if (attributes.category) body['category'] = attributes.category
-  if (attributes.licence) body['licence'] = attributes.licence
-  if (attributes.language) body['language'] = attributes.language
-  if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
-  if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
-  if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
-  if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
-  if (attributes.description) body['description'] = attributes.description
-  if (attributes.tags) body['tags'] = attributes.tags
-  if (attributes.privacy) body['privacy'] = attributes.privacy
-  if (attributes.channelId) body['channelId'] = attributes.channelId
-  if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
-
-  // Upload request
-  if (attributes.thumbnailfile || attributes.previewfile) {
-    const attaches: any = {}
-    if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
-    if (attributes.previewfile) attaches.previewfile = attributes.previewfile
-
-    return makeUploadRequest({
-      url,
-      method: 'PUT',
-      path,
-      token: accessToken,
-      fields: body,
-      attaches,
-      statusCodeExpected
-    })
-  }
-
-  return makePutBodyRequest({
-    url,
-    path,
-    fields: body,
-    token: accessToken,
-    statusCodeExpected
-  })
-}
-
-function rateVideo (url: string, accessToken: string, id: number | string, rating: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
-  const path = '/api/v1/videos/' + id + '/rate'
-
-  return request(url)
-          .put(path)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + accessToken)
-          .send({ rating })
-          .expect(specialStatus)
-}
-
-function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
-  return new Promise<any>((res, rej) => {
-    const torrentName = videoUUID + '-' + resolution + '.torrent'
-    const torrentPath = buildServerDirectory(server, join('torrents', torrentName))
-
-    readFile(torrentPath, (err, data) => {
-      if (err) return rej(err)
-
-      return res(parseTorrent(data))
-    })
-  })
+    ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus })
+    : server.videos.buildResumeUpload({ token, attributes, expectedStatus })
 }
 
 async function completeVideoCheck (
-  url: string,
+  server: PeerTubeServer,
   video: any,
   attributes: {
     name: string
@@ -682,7 +128,7 @@ async function completeVideoCheck (
   if (!attributes.likes) attributes.likes = 0
   if (!attributes.dislikes) attributes.dislikes = 0
 
-  const host = new URL(url).host
+  const host = new URL(server.url).host
   const originHost = attributes.account.host
 
   expect(video.name).to.equal(attributes.name)
@@ -719,8 +165,7 @@ async function completeVideoCheck (
     expect(video.originallyPublishedAt).to.be.null
   }
 
-  const res = await getVideo(url, video.uuid)
-  const videoDetails: VideoDetails = res.body
+  const videoDetails = await server.videos.get({ id: video.uuid })
 
   expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
   expect(videoDetails.tags).to.deep.equal(attributes.tags)
@@ -745,18 +190,16 @@ async function completeVideoCheck (
 
     expect(file.magnetUri).to.have.lengthOf.above(2)
 
-    expect(file.torrentDownloadUrl).to.equal(`http://${host}/download/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
-    expect(file.torrentUrl).to.equal(`http://${host}/lazy-static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
+    expect(file.torrentDownloadUrl).to.match(new RegExp(`http://${host}/download/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
+    expect(file.torrentUrl).to.match(new RegExp(`http://${host}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
 
-    expect(file.fileUrl).to.equal(`http://${originHost}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
-    expect(file.fileDownloadUrl).to.equal(`http://${originHost}/download/videos/${videoDetails.uuid}-${file.resolution.id}${extension}`)
+    expect(file.fileUrl).to.match(new RegExp(`http://${originHost}/static/webseed/${uuidRegex}-${file.resolution.id}${extension}`))
+    expect(file.fileDownloadUrl).to.match(new RegExp(`http://${originHost}/download/videos/${uuidRegex}-${file.resolution.id}${extension}`))
 
     await Promise.all([
       makeRawRequest(file.torrentUrl, 200),
       makeRawRequest(file.torrentDownloadUrl, 200),
-      makeRawRequest(file.metadataUrl, 200),
-      // Backward compatibility
-      makeRawRequest(`http://${originHost}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`, 200)
+      makeRawRequest(file.metadataUrl, 200)
     ])
 
     expect(file.resolution.id).to.equal(attributeFile.resolution)
@@ -776,149 +219,34 @@ async function completeVideoCheck (
   }
 
   expect(videoDetails.thumbnailPath).to.exist
-  await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
+  await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
 
   if (attributes.previewfile) {
     expect(videoDetails.previewPath).to.exist
-    await testImage(url, attributes.previewfile, videoDetails.previewPath)
+    await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
   }
 }
 
-async function videoUUIDToId (url: string, id: number | string) {
-  if (validator.isUUID('' + id) === false) return id
-
-  const res = await getVideo(url, id)
-  return res.body.id
-}
-
-async function uploadVideoAndGetId (options: {
-  server: ServerInfo
-  videoName: string
-  nsfw?: boolean
-  privacy?: VideoPrivacy
-  token?: string
-  fixture?: string
-}) {
-  const videoAttrs: any = { name: options.videoName }
-  if (options.nsfw) videoAttrs.nsfw = options.nsfw
-  if (options.privacy) videoAttrs.privacy = options.privacy
-  if (options.fixture) videoAttrs.fixture = options.fixture
-
-  const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
-
-  return res.body.video as { id: number, uuid: string, shortUUID: string }
-}
-
-async function getLocalIdByUUID (url: string, uuid: string) {
-  const res = await getVideo(url, uuid)
-
-  return res.body.id
-}
-
 // serverNumber starts from 1
-async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) {
+async function uploadRandomVideoOnServers (
+  servers: PeerTubeServer[],
+  serverNumber: number,
+  additionalParams?: VideoEdit & { prefixName?: string }
+) {
   const server = servers.find(s => s.serverNumber === serverNumber)
-  const res = await uploadRandomVideo(server, false, additionalParams)
+  const res = await server.videos.randomUpload({ wait: false, additionalParams })
 
   await waitJobs(servers)
 
   return res
 }
 
-async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
-  const prefixName = additionalParams.prefixName || ''
-  const name = prefixName + buildUUID()
-
-  const data = Object.assign({ name }, additionalParams)
-  const res = await uploadVideo(server.url, server.accessToken, data)
-
-  if (wait) await waitJobs([ server ])
-
-  return { uuid: res.body.video.uuid, name }
-}
-
 // ---------------------------------------------------------------------------
 
 export {
-  getVideoDescription,
-  getVideoCategories,
-  uploadRandomVideo,
-  getVideoLicences,
-  videoUUIDToId,
-  getVideoPrivacies,
-  getVideoLanguages,
-  getMyVideos,
-  getAccountVideos,
-  getVideoChannelVideos,
-  getVideo,
-  getVideoFileMetadataUrl,
-  getVideoWithToken,
-  getVideosList,
-  removeAllVideos,
   checkUploadVideoParam,
-  getVideosListPagination,
-  getVideosListSort,
-  removeVideo,
-  getVideosListWithToken,
-  uploadVideo,
-  sendResumableChunks,
-  getVideosWithFilters,
-  uploadRandomVideoOnServers,
-  updateVideo,
-  rateVideo,
-  viewVideo,
-  parseTorrentVideo,
-  getLocalVideos,
   completeVideoCheck,
+  uploadRandomVideoOnServers,
   checkVideoFilesWereRemoved,
-  getPlaylistVideos,
-  getMyVideosWithFilter,
-  uploadVideoAndGetId,
-  getLocalIdByUUID,
-  getVideoIdFromUUID,
-  prepareResumableUpload
-}
-
-// ---------------------------------------------------------------------------
-
-function buildUploadReq (req: request.Test, attributes: VideoAttributes) {
-
-  for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) {
-    if (attributes[key] !== undefined) {
-      req.field(key, attributes[key])
-    }
-  }
-
-  for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) {
-    if (attributes[key] !== undefined) {
-      req.field(key, JSON.stringify(attributes[key]))
-    }
-  }
-
-  for (const key of [ 'language', 'privacy', 'category', 'licence' ]) {
-    if (attributes[key] !== undefined) {
-      req.field(key, attributes[key].toString())
-    }
-  }
-
-  const tags = attributes.tags || []
-  for (let i = 0; i < tags.length; i++) {
-    req.field('tags[' + i + ']', attributes.tags[i])
-  }
-
-  for (const key of [ 'thumbnailfile', 'previewfile' ]) {
-    if (attributes[key] !== undefined) {
-      req.attach(key, buildAbsoluteFixturePath(attributes[key]))
-    }
-  }
-
-  if (attributes.scheduleUpdate) {
-    if (attributes.scheduleUpdate.updateAt) {
-      req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
-    }
-
-    if (attributes.scheduleUpdate.privacy) {
-      req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
-    }
-  }
+  saveVideoInServers
 }
diff --git a/shared/models/http/index.ts b/shared/models/http/index.ts
new file mode 100644 (file)
index 0000000..ec991af
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './http-error-codes'
+export * from './http-methods'
index 5c2bc480ee3f4ab938f74b2c8ee6e1bec0a1b44a..78723d83046223df4a9d21e2a3913a08e71827f2 100644 (file)
@@ -4,6 +4,7 @@ export * from './bulk'
 export * from './common'
 export * from './custom-markup'
 export * from './feeds'
+export * from './http'
 export * from './joinpeertube'
 export * from './moderation'
 export * from './overviews'
index 4703c0a8bb11d3a971df126cf3d12369015b83c8..35247c1e371f43895ae74f2cb28fb599c31ccf22 100644 (file)
@@ -1,8 +1,12 @@
 import { VideoPlaylistPrivacy } from '../../../videos/playlist/video-playlist-privacy.model'
+import { ConstantManager } from '../plugin-constant-manager.model'
 
-export interface PluginPlaylistPrivacyManager {
-  // PUBLIC = 1,
-  // UNLISTED = 2,
-  // PRIVATE = 3
+export interface PluginPlaylistPrivacyManager extends ConstantManager<VideoPlaylistPrivacy> {
+  /**
+   * PUBLIC = 1,
+   * UNLISTED = 2,
+   * PRIVATE = 3
+   * @deprecated use `deleteConstant` instead
+   */
   deletePlaylistPrivacy: (privacyKey: VideoPlaylistPrivacy) => boolean
 }
index 201bfa979b9fd4d1a3c0a99da6924c6f0d5a73e1..cf3d828fe38d542eb5972b6a7adb677bec849e20 100644 (file)
@@ -1,5 +1,13 @@
-export interface PluginVideoCategoryManager {
+import { ConstantManager } from '../plugin-constant-manager.model'
+
+export interface PluginVideoCategoryManager extends ConstantManager<number> {
+  /**
+   * @deprecated use `addConstant` instead
+   */
   addCategory: (categoryKey: number, categoryLabel: string) => boolean
 
+  /**
+   * @deprecated use `deleteConstant` instead
+   */
   deleteCategory: (categoryKey: number) => boolean
 }
index 3fd577a79e807a71e8da9a190463e393bdc2bc45..69fc8e5034d03e7990240ebbb83f3779ff5fd41c 100644 (file)
@@ -1,5 +1,13 @@
-export interface PluginVideoLanguageManager {
+import { ConstantManager } from '../plugin-constant-manager.model'
+
+export interface PluginVideoLanguageManager extends ConstantManager<string> {
+  /**
+   * @deprecated use `addConstant` instead
+   */
   addLanguage: (languageKey: string, languageLabel: string) => boolean
 
+  /**
+   * @deprecated use `deleteConstant` instead
+   */
   deleteLanguage: (languageKey: string) => boolean
 }
index 82a634d3a9b55c748aac96a1cc177674657bd744..6efeadd7d29b3b0388637bbdc25129a5c7cdbb55 100644 (file)
@@ -1,5 +1,13 @@
-export interface PluginVideoLicenceManager {
+import { ConstantManager } from '../plugin-constant-manager.model'
+
+export interface PluginVideoLicenceManager extends ConstantManager<number> {
+  /**
+   * @deprecated use `addLicence` instead
+   */
   addLicence: (licenceKey: number, licenceLabel: string) => boolean
 
+  /**
+   * @deprecated use `deleteLicence` instead
+   */
   deleteLicence: (licenceKey: number) => boolean
 }
index 7717115e3e24ed52751819145f29fb0a0a363f70..a237037db279964f931a6dc5f40d065e50d014c7 100644 (file)
@@ -1,9 +1,13 @@
 import { VideoPrivacy } from '../../../videos/video-privacy.enum'
+import { ConstantManager } from '../plugin-constant-manager.model'
 
-export interface PluginVideoPrivacyManager {
-  // PUBLIC = 1
-  // UNLISTED = 2
-  // PRIVATE = 3
-  // INTERNAL = 4
+export interface PluginVideoPrivacyManager extends ConstantManager<VideoPrivacy> {
+  /**
+   * PUBLIC = 1,
+   * UNLISTED = 2,
+   * PRIVATE = 3
+   * INTERNAL = 4
+   * @deprecated use `deleteConstant` instead
+   */
   deletePrivacy: (privacyKey: VideoPrivacy) => boolean
 }
diff --git a/shared/models/plugins/server/plugin-constant-manager.model.ts b/shared/models/plugins/server/plugin-constant-manager.model.ts
new file mode 100644 (file)
index 0000000..4de3ce3
--- /dev/null
@@ -0,0 +1,7 @@
+export interface ConstantManager <K extends string | number> {
+  addConstant: (key: K, label: string) => boolean
+  deleteConstant: (key: K) => boolean
+  getConstantValue: (key: K) => string
+  getConstants: () => Record<K, string>
+  resetConstants: () => void
+}
index 5f29534b585a9a784797318a91a38d1365b106ef..562c6eb126deefa00c07d4c9ea9accfa39ea1ccc 100644 (file)
@@ -18,6 +18,10 @@ export const serverFilterHookObject = {
   'filter:api.user.me.videos.list.params': true,
   'filter:api.user.me.videos.list.result': true,
 
+  // Filter params/result used to list overview videos for the REST API
+  'filter:api.overviews.videos.list.params': true,
+  'filter:api.overviews.videos.list.result': true,
+
   // Filter params/results to search videos/channels in the DB or on the remote index
   'filter:api.search.videos.local.list.params': true,
   'filter:api.search.videos.local.list.result': true,
index 8f93c4bd5aee2edefdc3bc9492ab781f29a58b3b..b68a1e80b4e3f81cd9b1483d976a488d432250c9 100644 (file)
@@ -1,9 +1,18 @@
 import { SearchTargetQuery } from './search-target-query.model'
 
 export interface VideoChannelsSearchQuery extends SearchTargetQuery {
-  search: string
+  search?: string
 
   start?: number
   count?: number
   sort?: string
+
+  host?: string
+  handles?: string[]
+}
+
+export interface VideoChannelsSearchQueryAfterSanitize extends VideoChannelsSearchQuery {
+  start: number
+  count: number
+  sort: string
 }
index 31f05218e16961a7debbdc1b197b2440ac98243d..d9027eb5bcc8b0be04e1ff7e0d17ce2de268ab34 100644 (file)
@@ -1,9 +1,20 @@
 import { SearchTargetQuery } from './search-target-query.model'
 
 export interface VideoPlaylistsSearchQuery extends SearchTargetQuery {
-  search: string
+  search?: string
 
   start?: number
   count?: number
   sort?: string
+
+  host?: string
+
+  // UUIDs or short UUIDs
+  uuids?: string[]
+}
+
+export interface VideoPlaylistsSearchQueryAfterSanitize extends VideoPlaylistsSearchQuery {
+  start: number
+  count: number
+  sort: string
 }
index bd02489ea4a7cce7a65bb052f888b9d117012b66..2f2e9a9348f43a6c2f1e348ea41e5e527e40dc27 100644 (file)
@@ -21,6 +21,14 @@ export interface VideosCommonQuery {
   tagsAllOf?: string[]
 
   filter?: VideoFilter
+
+  skipCount?: boolean
+}
+
+export interface VideosCommonQueryAfterSanitize extends VideosCommonQuery {
+  start: number
+  count: number
+  sort: string
 }
 
 export interface VideosWithSearchCommonQuery extends VideosCommonQuery {
index 406f6cab22b808dec9d456b48c358c2ef3c199a5..a5436879d5fda9ecd50b3cf2ab8d2f47ef454511 100644 (file)
@@ -4,6 +4,8 @@ import { VideosCommonQuery } from './videos-common-query.model'
 export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery {
   search?: string
 
+  host?: string
+
   startDate?: string // ISO 8601
   endDate?: string // ISO 8601
 
@@ -12,4 +14,13 @@ export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery
 
   durationMin?: number // seconds
   durationMax?: number // seconds
+
+  // UUIDs or short UUIDs
+  uuids?: string[]
+}
+
+export interface VideosSearchQueryAfterSanitize extends VideosSearchQuery {
+  start: number
+  count: number
+  sort: string
 }
index 7ceff91372a9210e7ba8684661aa751b1bee1f1e..2ecabdecab1e8ad47541a809afdc35ca33886036 100644 (file)
@@ -1,5 +1,6 @@
 export interface Debug {
   ip: string
+  activityPubMessagesWaiting: number
 }
 
 export interface SendDebugCommand {
index 06bf5c5990c635815c74dc0483ce54320a598a24..0f7646c7afcfbb0899fc34a7b7149754e6cfd01a 100644 (file)
@@ -10,4 +10,5 @@ export * from './peertube-problem-document.model'
 export * from './server-config.model'
 export * from './server-debug.model'
 export * from './server-error-code.enum'
+export * from './server-follow-create.model'
 export * from './server-stats.model'
index e391d5aadb59fde41e82400dadbe3b7acd26c8e3..8dd96f7a377df59372f41ac3deea86333b3e0881 100644 (file)
@@ -1,4 +1,4 @@
-import { HttpStatusCode } from '../../core-utils'
+import { HttpStatusCode } from '../../models'
 import { OAuth2ErrorCode, ServerErrorCode } from './server-error-code.enum'
 
 export interface PeerTubeProblemDocumentData {
diff --git a/shared/models/server/server-follow-create.model.ts b/shared/models/server/server-follow-create.model.ts
new file mode 100644 (file)
index 0000000..3f90c7d
--- /dev/null
@@ -0,0 +1,4 @@
+export interface ServerFollowCreate {
+  hosts?: string[]
+  handles?: string[]
+}
index a9d5780540413e1fe41eb895540eaa01be8a4d57..b61a8cd40a5fa23b6f2c49f6b0c2b2386c184be5 100644 (file)
@@ -1,3 +1,4 @@
+export * from './user-create-result.model'
 export * from './user-create.model'
 export * from './user-flag.model'
 export * from './user-login.model'
diff --git a/shared/models/users/user-create-result.model.ts b/shared/models/users/user-create-result.model.ts
new file mode 100644 (file)
index 0000000..835b241
--- /dev/null
@@ -0,0 +1,7 @@
+export interface UserCreateResult {
+  id: number
+
+  account: {
+    id: number
+  }
+}
index 8b33e3fbdab30d66a90719ba315a0ea06ff68bc5..5820589fe8a999fe7395288007af1412cba0b0e1 100644 (file)
@@ -36,6 +36,7 @@ export const enum UserNotificationType {
 export interface VideoInfo {
   id: number
   uuid: string
+  shortUUID: string
   name: string
 }
 
@@ -82,11 +83,7 @@ export interface UserNotification {
     comment?: {
       threadId: number
 
-      video: {
-        id: number
-        uuid: string
-        name: string
-      }
+      video: VideoInfo
     }
 
     account?: ActorInfo
index 9dbaa42da9024e86e356eefbe2cfda4629de875d..6cdabffbd67bd3990a8d1960ad3eb3b0779daf1b 100644 (file)
@@ -1,3 +1,4 @@
+export * from './video-channel-create-result.model'
 export * from './video-channel-create.model'
 export * from './video-channel-update.model'
 export * from './video-channel.model'
diff --git a/shared/models/videos/channel/video-channel-create-result.model.ts b/shared/models/videos/channel/video-channel-create-result.model.ts
new file mode 100644 (file)
index 0000000..e3d7aeb
--- /dev/null
@@ -0,0 +1,3 @@
+export interface VideoChannelCreateResult {
+  id: number
+}
index 7b9261a36c7b007e737de3cf08a492cf3e88b052..80c6c0724d4f22a9c24819469a9ae457bea9e39d 100644 (file)
@@ -1 +1,2 @@
+export * from './video-comment-create.model'
 export * from './video-comment.model'
diff --git a/shared/models/videos/comment/video-comment-create.model.ts b/shared/models/videos/comment/video-comment-create.model.ts
new file mode 100644 (file)
index 0000000..1f01354
--- /dev/null
@@ -0,0 +1,3 @@
+export interface VideoCommentCreate {
+  text: string
+}
index 79c0e4c0a87996cb89b0ceec19a361888cb0eb69..737cfe098480737a09372cc26836f3d178851ad9 100644 (file)
@@ -1,3 +1,4 @@
+import { ResultList } from '../../common'
 import { Account } from '../../actors'
 
 export interface VideoComment {
@@ -36,11 +37,9 @@ export interface VideoCommentAdmin {
   }
 }
 
+export type VideoCommentThreads = ResultList<VideoComment> & { totalNotDeletedComments: number }
+
 export interface VideoCommentThreadTree {
   comment: VideoComment
   children: VideoCommentThreadTree[]
 }
-
-export interface VideoCommentCreate {
-  text: string
-}
index f11a4bd28ba66124f6a7285f28845388d223d567..a9e8ce496c608c4b251e0e3307a611100da07118 100644 (file)
@@ -1,6 +1,7 @@
 export * from './video-exist-in-playlist.model'
 export * from './video-playlist-create-result.model'
 export * from './video-playlist-create.model'
+export * from './video-playlist-element-create-result.model'
 export * from './video-playlist-element-create.model'
 export * from './video-playlist-element-update.model'
 export * from './video-playlist-element.model'
diff --git a/shared/models/videos/playlist/video-playlist-element-create-result.model.ts b/shared/models/videos/playlist/video-playlist-element-create-result.model.ts
new file mode 100644 (file)
index 0000000..dc475e7
--- /dev/null
@@ -0,0 +1,3 @@
+export interface VideoPlaylistElementCreateResult {
+  id: number
+}
index e21ccae04ed55a1dbab53b5ef0e872aed1813b6e..86653b959a73b257e4fa2db8b2d1d6a7ff06eda9 100644 (file)
@@ -1,5 +1,6 @@
 import { VideoPrivacy } from './video-privacy.enum'
 import { VideoScheduleUpdate } from './video-schedule-update.model'
+
 export interface VideoUpdate {
   name?: string
   category?: number
index 99a725ead0b37f3bc62e436108be060d237323a5..76e78fe53a65fc12f4b51a754a18c76ed0de97d4 100644 (file)
@@ -716,7 +716,7 @@ paths:
           - admin
       tags:
         - Instance Follows
-      summary: Follow a list of servers
+      summary: Follow a list of actors (PeerTube instance, channel or account)
       responses:
         '204':
           description: successful operation
@@ -734,28 +734,32 @@ paths:
                     type: string
                     format: hostname
                   uniqueItems: true
+                handles:
+                  type: array
+                  items:
+                    type: string
+                  uniqueItems: true
 
-  '/server/following/{host}':
+  '/server/following/{hostOrHandle}':
     delete:
-      summary: Unfollow a server
+      summary: Unfollow an actor (PeerTube instance, channel or account)
       security:
         - OAuth2:
           - admin
       tags:
         - Instance Follows
       parameters:
-        - name: host
+        - name: hostOrHandle
           in: path
           required: true
-          description: The host to unfollow
+          description: The hostOrHandle to unfollow
           schema:
             type: string
-            format: hostname
       responses:
         '204':
           description: successful operation
         '404':
-          description: host not found
+          description: host or handle not found
 
   /users:
     post:
index 8ea0c047de2adfdab47adb0e8e52580c12891dd6..d6c084cd754c93457374248552d507e6b5056f7a 100644 (file)
@@ -151,6 +151,51 @@ sudo systemctl enable --now redis
 sudo systemctl enable --now postgresql
 ```
 
+## Rocky Linux 8.4  
+
+1. Pull the latest updates:  
+```
+sudo dnf update -y
+```
+
+2. Install NodeJS 12.x (why 12 and not 14? Not sure...):  
+```
+sudo dnf module install -y nodejs:12
+```
+
+3. Install yarn:  
+```
+sudo npm install --global yarn
+```
+
+4. Install or compile ffmpeg (if you want to compile... enjoy):  
+```
+sudo dnf install -y epel-release 
+sudo dnf --enablerepo=powertools install -y SDL2 SDL2-devel
+sudo dnf install -y --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-8.noarch.rpm
+sudo dnf install -y ffmpeg
+sudo dnf update -y
+```
+
+5. Install PostgreSQL and Python3 and other stuff:
+```
+sudo dnf install -y nginx postgresql postgresql-server postgresql-contrib openssl gcc-c++ make wget redis git python3
+sudo ln -s /usr/bin/python3 /usr/bin/python
+sudo PGSETUP_INITDB_OPTIONS='--auth-host=md5' postgresql-setup --initdb --unit postgresql
+sudo systemctl enable --now redis
+sudo systemctl enable --now postgresql
+```
+
+6. Configure the peertube user:
+```
+sudo useradd -m -d /var/www/peertube -s /bin/bash -p peertube peertube
+```
+
+7. Unknown missing steps:
+- Steps missing here... these were adapted from the CentOS 8 steps which abruptly ended.  
+- /var/www/peertube does not exist yet (expected? done in future steps? documentation?).  
+- Nothing about Certbot, NGINX, Firewall settings, and etc.  
+- Hopefully someone can suggest what is missing here with some hints so I can add it?  
 
 ## Fedora
 
index 568c0662f8bd3eb00b27f3737c068218e291d4e2..85aaf9f02ffe3300a0d2bb42bb9b25abe0a8e3a2 100644 (file)
@@ -234,21 +234,29 @@ function register ({
 
 #### Update video constants
 
-You can add/delete video categories, licences or languages using the appropriate managers:
+You can add/delete video categories, licences or languages using the appropriate constant managers:
 
 ```js
-function register (...) {
-  videoLanguageManager.addLanguage('al_bhed', 'Al Bhed')
-  videoLanguageManager.deleteLanguage('fr')
+function register ({ 
+  videoLanguageManager, 
+  videoCategoryManager, 
+  videoLicenceManager, 
+  videoPrivacyManager, 
+  playlistPrivacyManager 
+}) {
+  videoLanguageManager.addConstant('al_bhed', 'Al Bhed')
+  videoLanguageManager.deleteConstant('fr')
 
-  videoCategoryManager.addCategory(42, 'Best category')
-  videoCategoryManager.deleteCategory(1) // Music
+  videoCategoryManager.addConstant(42, 'Best category')
+  videoCategoryManager.deleteConstant(1) // Music
+  videoCategoryManager.resetConstants() // Reset to initial categories
+  videoCategoryManager.getConstants() // Retrieve all category constants
 
-  videoLicenceManager.addLicence(42, 'Best licence')
-  videoLicenceManager.deleteLicence(7) // Public domain
+  videoLicenceManager.addConstant(42, 'Best licence')
+  videoLicenceManager.deleteConstant(7) // Public domain
 
-  videoPrivacyManager.deletePrivacy(2) // Remove Unlisted video privacy
-  playlistPrivacyManager.deletePlaylistPrivacy(3) // Remove Private video playlist privacy
+  videoPrivacyManager.deleteConstant(2) // Remove Unlisted video privacy
+  playlistPrivacyManager.deleteConstant(3) // Remove Private video playlist privacy
 }
 ```
 
index d305722c465456b57b7143ef56845b0f8b8b46a4..32e4a42e4655f0c944977a7dab0422abe8154034 100644 (file)
@@ -8,6 +8,7 @@
     "emitDecoratorMetadata": true,
     "importHelpers": true,
     "removeComments": true,
+    "strictBindCallApply": true,
     "outDir": "./dist",
     "lib": [
       "dom",
index f68741038697eb827f5e3c28ed589861dffa67ed..68f17d4143a1ea0141396d732266e11f0e32a1fb 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   dependencies:
     "@babel/highlight" "^7.14.5"
 
-"@babel/helper-validator-identifier@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8"
-  integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==
+"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.8":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz#32be33a756f29e278a0d644fa08a2c9e0f88a34c"
+  integrity sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow==
 
 "@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5":
   version "7.14.5"
     js-tokens "^4.0.0"
 
 "@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
-  version "7.14.7"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.7.tgz#6099720c8839ca865a2637e6c85852ead0bdb595"
-  integrity sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.8.tgz#66fd41666b2d7b840bd5ace7f7416d5ac60208d4"
+  integrity sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA==
 
 "@babel/runtime@^7.7.2":
-  version "7.14.6"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d"
-  integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446"
+  integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==
   dependencies:
     regenerator-runtime "^0.13.4"
 
 "@babel/types@^7.6.1", "@babel/types@^7.9.6":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.5.tgz#3bb997ba829a2104cedb20689c4a5b8121d383ff"
-  integrity sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.8.tgz#38109de8fcadc06415fbd9b74df0065d4d41c728"
+  integrity sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q==
   dependencies:
-    "@babel/helper-validator-identifier" "^7.14.5"
+    "@babel/helper-validator-identifier" "^7.14.8"
     to-fast-properties "^2.0.0"
 
 "@dabh/diagnostics@^2.0.2":
     kuler "^2.0.0"
 
 "@digitalbazaar/http-client@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-1.1.0.tgz#cac383b24ace04b18b919deab773462b03d3d7b0"
-  integrity sha512-ks7hqa6hm9NyULdbm9qL6TRS8rADzBw8R0lETvUgvdNXu9H62XG2YqoKRDThtfgWzWxLwRJ3Z2o4ev81dZZbyQ==
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-1.2.0.tgz#1ea3661e77000a15bd892a294f20dc6cc5d1c93b"
+  integrity sha512-W9KQQ5pUJcaR0I4c2HPJC0a7kRbZApIorZgPnEDwMBgj16iQzutGLrCXYaZOmxqVLVNqqlQ4aUJh+HBQZy4W6Q==
   dependencies:
     esm "^3.2.22"
     ky "^0.25.1"
     ky-universal "^0.8.2"
 
-"@eslint/eslintrc@^0.4.2":
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.2.tgz#f63d0ef06f5c0c57d76c4ab5f63d3835c51b0179"
-  integrity sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==
+"@eslint/eslintrc@^0.4.3":
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
+  integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==
   dependencies:
     ajv "^6.12.4"
     debug "^4.1.1"
     strip-json-comments "^3.1.1"
 
 "@hapi/boom@^9.1.2":
-  version "9.1.2"
-  resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.2.tgz#48bd41d67437164a2d636e3b5bc954f8c8dc5e38"
-  integrity sha512-uJEJtiNHzKw80JpngDGBCGAmWjBtzxDCz17A9NO2zCi8LLBlb5Frpq4pXwyN+2JQMod4pKz5BALwyneCgDg89Q==
+  version "9.1.3"
+  resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.3.tgz#22cad56e39b7a4819161a99b1db19eaaa9b6cc6e"
+  integrity sha512-RlrGyZ603hE/eRTZtTltocRm50HHmrmL3kGOP0SQ9MasazlW1mt/fkv4C5P/6rnpFXjwld/POFX1C8tMZE3ldg==
   dependencies:
     "@hapi/hoek" "9.x.x"
 
   resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
   integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==
 
+"@humanwhocodes/config-array@^0.5.0":
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
+  integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==
+  dependencies:
+    "@humanwhocodes/object-schema" "^1.2.0"
+    debug "^4.1.1"
+    minimatch "^3.0.4"
+
+"@humanwhocodes/object-schema@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
+  integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
+
 "@jimp/bmp@^0.16.1":
   version "0.16.1"
   resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.16.1.tgz#6e2da655b2ba22e721df0795423f34e92ef13768"
   integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
 
 "@nodelib/fs.walk@^1.2.3":
-  version "1.2.7"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz#94c23db18ee4653e129abd26fb06f870ac9e1ee2"
-  integrity sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
   dependencies:
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
     node-fetch "^2.6.1"
 
 "@openapitools/openapi-generator-cli@^2.1.4":
-  version "2.3.5"
-  resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.3.5.tgz#20f3974879ae22beb18a0f5a3685cabacf380cef"
-  integrity sha512-b9dX47j3+g08qM/EMg/Ftw2qBOpfhKB31xyPJ7+kBvGvcoNoMed3aPyojv1iWNfU1KlJvp6k9zJvViOND0ckGg==
+  version "2.3.7"
+  resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.3.7.tgz#5aaf9d178545874828db7c7c84a6f9a5b8ea00a4"
+  integrity sha512-4B93yPXop44fhAw6CT0eciaA0dRbPw5W7p2ZZ+HZ1uk4UD50zgaTAa9pnrsnCDnNtw2cvbZM0uCOf8xQyUy8Vg==
   dependencies:
     "@nestjs/common" "7.6.18"
     "@nestjs/core" "7.6.18"
     console.table "0.10.0"
     fs-extra "10.0.0"
     glob "7.1.6"
-    inquirer "8.1.1"
+    inquirer "8.1.2"
     lodash "4.17.21"
     reflect-metadata "0.1.13"
-    rxjs "7.1.0"
+    rxjs "7.2.0"
     tslib "1.13.0"
 
 "@sindresorhus/is@^0.14.0":
     defer-to-connect "^1.0.1"
 
 "@szmarczak/http-timer@^4.0.5":
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152"
-  integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807"
+  integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==
   dependencies:
     defer-to-connect "^2.0.0"
 
   integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
 
 "@tsconfig/node16@^1.0.1":
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.1.tgz#a6ca6a9a0ff366af433f42f5f0e124794ff6b8f1"
-  integrity sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA==
-
-"@types/apicache@^1.2.0":
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/@types/apicache/-/apicache-1.2.2.tgz#b820659b1d95e66ec0e71dcd0317e9d30f0c154b"
-  integrity sha512-+rjhMdbx7m7pKnNiQCSiKE21mGmMzsfCU871h5BCj4guhAj423j61Dq0Yrr9CnLiDwUhSKtkXWudr9SQE0/IoA==
-  dependencies:
-    "@types/redis" "*"
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
+  integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
 
 "@types/async-lock@^1.1.0":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-1.1.2.tgz#cbc26a34b11b83b28f7783a843c393b443ef8bef"
-  integrity sha512-j9n4bb6RhgFIydBe0+kpjnBPYumDaDyU8zvbWykyVMkku+c2CSu31MZkLeaBfqIwU+XCxlDpYDfyMQRkM0AkeQ==
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-1.1.3.tgz#0d86017cf87abbcb941c55360e533d37a3f23b3d"
+  integrity sha512-UpeDcjGKsYEQMeqEbfESm8OWJI305I7b9KE4ji3aBjoKWyN5CTdn8izcA1FM1DVDne30R5fNEnIy89vZw5LXJQ==
 
 "@types/async@^3.0.0":
-  version "3.2.6"
-  resolved "https://registry.yarnpkg.com/@types/async/-/async-3.2.6.tgz#1d49339846c6aa0b0a8a08303c21176f1b64dc4f"
-  integrity sha512-ZkrXnZLC1mc4b9QLKaSrsxV4oxTRs10OI2kgSApT8G0v1jrmqppSHUVQ15kLorzsFBTjvf7OKF4kAibuuNQ+xA==
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/@types/async/-/async-3.2.7.tgz#f784478440d313941e7b12c2e4db53b0ed55637b"
+  integrity sha512-a+MBBfOTs3ShFMlbH9qsRVFkjIUunEtxrBT0gxRx1cntjKRg2WApuGmNYzHkwKaIhMi3SMbKktaD/rLObQMwIw==
 
 "@types/bcrypt@^5.0.0":
   version "5.0.0"
     "@types/node" "*"
 
 "@types/bluebird@^3.5.33":
-  version "3.5.35"
-  resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.35.tgz#3964c48372bf62d60616d8673dd77a9719ebac9b"
-  integrity sha512-2WeeXK7BuQo7yPI4WGOBum90SzF/f8rqlvpaXx4rjeTmNssGRDHWf7fgDUH90xMB3sUOu716fUK5d+OVx0+ncQ==
+  version "3.5.36"
+  resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.36.tgz#00d9301d4dc35c2f6465a8aec634bb533674c652"
+  integrity sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q==
 
 "@types/body-parser@*", "@types/body-parser@^1.16.3":
-  version "1.19.0"
-  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
-  integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c"
+  integrity sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==
   dependencies:
     "@types/connect" "*"
     "@types/node" "*"
 
 "@types/bull@^3.15.0":
-  version "3.15.1"
-  resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.1.tgz#3c3fd665b43ef383ca95a91b8d1448461fae0898"
-  integrity sha512-thZyjxikoyuDa/ptZEqtTEPUjwlDenkpPigpIyad1X5UMp7U0fXTLiDHJjZ/5yXmVPuWx0cXFXj3drmva/UJRA==
+  version "3.15.2"
+  resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.2.tgz#b824b0b4fc8d1d9294a20973f6ceedcba1a7f3e8"
+  integrity sha512-uMQ7u/4GxY2bSTMd4P2yLkyqu3GoKbwTCDkMHJJ2g9OkiMq0Vxw+C7lF4w+oNkwZzZ2k4Kw76Ncxjd6GMnc+CA==
   dependencies:
     "@types/ioredis" "*"
 
 "@types/bytes@^3.0.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@types/bytes/-/bytes-3.1.0.tgz#835a3e4aea3b4d7604aca216a78de372bff3ecc3"
-  integrity sha512-5YG1AiIC8HPPXRvYAIa7ehK3YMAwd0DWiPCtpuL9sgKceWLyWsVtLRA+lT4NkoanDNF9slwQ66lPizWDpgRlWA==
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/@types/bytes/-/bytes-3.1.1.tgz#67a876422e660dc4c10a27f3e5bcfbd5455f01d0"
+  integrity sha512-lOGyCnw+2JVPKU3wIV0srU0NyALwTBJlVSx5DfMQOFuuohA8y9S8orImpuIQikZ0uIQ8gehrRjxgQC1rLRi11w==
 
 "@types/cacheable-request@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976"
-  integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9"
+  integrity sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==
   dependencies:
     "@types/http-cache-semantics" "*"
     "@types/keyv" "*"
     "@types/chai" "*"
 
 "@types/chai@*", "@types/chai@^4.0.4":
-  version "4.2.19"
-  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.19.tgz#80f286b515897413c7a35bdda069cc80f2344233"
-  integrity sha512-jRJgpRBuY+7izT7/WNXP/LsMO9YonsstuL+xuvycDyESpoDoIAsMd7suwpB4h9oEWB+ZlPTqJJ8EHomzNhwTPQ==
+  version "4.2.21"
+  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.21.tgz#9f35a5643129df132cf3b5c1ec64046ea1af0650"
+  integrity sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==
 
 "@types/component-emitter@^1.2.10":
   version "1.2.10"
   resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea"
   integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==
 
-"@types/config@^0.0.38":
-  version "0.0.38"
-  resolved "https://registry.yarnpkg.com/@types/config/-/config-0.0.38.tgz#ca30679b21b5b297299467e3a3f1c8e2e64b9170"
-  integrity sha512-z2WizAfIFgSv8SQfQ8c0LlbDAcK47D/o93XW6bxZ9t3bs4fmmfAPjk1nhAIBTG84PBBCHfSPM+8g7vhLdbFokg==
+"@types/config@^0.0.39":
+  version "0.0.39"
+  resolved "https://registry.yarnpkg.com/@types/config/-/config-0.0.39.tgz#aad18ceb9439329adc3d4c6b91a908a72c715612"
+  integrity sha512-EBHj9lSIyw62vwqCwkeJXjiV6C2m2o+RJZlRWLkHduGYiNBoMXcY6AhSLqjQQ+uPdrPYrOMYvVa41zjo00LbFQ==
 
 "@types/connect@*":
-  version "3.4.34"
-  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901"
-  integrity sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==
+  version "3.4.35"
+  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
+  integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
   dependencies:
     "@types/node" "*"
 
 "@types/cookie@^0.4.0":
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108"
-  integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
+  integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
 
 "@types/cookiejar@*":
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8"
   integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==
 
-"@types/cors@^2.8.8":
-  version "2.8.10"
-  resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4"
-  integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==
+"@types/cors@^2.8.10":
+  version "2.8.12"
+  resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
+  integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
 
 "@types/express-rate-limit@^5.0.0":
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/@types/express-rate-limit/-/express-rate-limit-5.1.2.tgz#51f030ce722fe298269f85378b49a34837a1d2ca"
-  integrity sha512-loN1dcr0WEKsbVtXNQKDef4Fmh25prfy+gESrwTDofIhAt17ngTxrsDiEZxLemmfHH279x206CdUB9XHXS9E6Q==
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/@types/express-rate-limit/-/express-rate-limit-5.1.3.tgz#79f2ca40d90455a5798da6f8e06d8a3d35f4a1d6"
+  integrity sha512-H+TYy3K53uPU2TqPGFYaiWc2xJV6+bIFkDd/Ma2/h67Pa6ARk9kWE0p/K9OH1Okm0et9Sfm66fmXoAxsH2PHXg==
   dependencies:
     "@types/express" "*"
 
 "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18":
-  version "4.17.21"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.21.tgz#a427278e106bca77b83ad85221eae709a3414d42"
-  integrity sha512-gwCiEZqW6f7EoR8TTEfalyEhb1zA5jQJnRngr97+3pzMaO1RKoI1w2bw07TK72renMUVWcWS5mLI6rk1NqN0nA==
+  version "4.17.24"
+  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07"
+  integrity sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==
   dependencies:
     "@types/node" "*"
     "@types/qs" "*"
     "@types/range-parser" "*"
 
 "@types/express@*":
-  version "4.17.12"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.12.tgz#4bc1bf3cd0cfe6d3f6f2853648b40db7d54de350"
-  integrity sha512-pTYas6FrP15B1Oa0bkN5tQMNqOcVXa9j4FTFtO8DWI9kppKib+6NJtfTOOLcwxuuYvcX2+dVG6et1SxW/Kc17Q==
+  version "4.17.13"
+  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
+  integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
   dependencies:
     "@types/body-parser" "*"
     "@types/express-serve-static-core" "^4.17.18"
     "@types/serve-static" "*"
 
 "@types/fluent-ffmpeg@^2.1.16":
-  version "2.1.17"
-  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.17.tgz#6958dda400fe1b33c21f3683db76905cb210d053"
-  integrity sha512-/bdvjKw/mtBHlJ2370d04nt4CsWqU5MrwS/NtO96V01jxitJ4+iq8OFNcqc5CegeV3TQOK3uueK02kvRK+zjUg==
+  version "2.1.18"
+  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.18.tgz#72246c2f8c0f0a590aefc1cdbe13736c73f22a81"
+  integrity sha512-LTteOx3RUmnPlKkvhvW9aGOHdJYyEtIiRBVbYVO/zPU65ZYQelbPJ+zBBT+IXup7doMvxVstX7NMoUTWKZOhww==
   dependencies:
     "@types/node" "*"
 
 "@types/fs-extra@^9.0.1":
-  version "9.0.11"
-  resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.11.tgz#8cc99e103499eab9f347dbc6ca4e99fb8d2c2b87"
-  integrity sha512-mZsifGG4QeQ7hlkhO56u7zt/ycBgGxSVsFI/6lGTU34VtwkiqrrSDgw0+ygs8kFGWcXnFQWMrzF2h7TtDFNixA==
+  version "9.0.12"
+  resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.12.tgz#9b8f27973df8a7a3920e8461517ebf8a7d4fdfaf"
+  integrity sha512-I+bsBr67CurCGnSenZZ7v94gd3tc3+Aj2taxMT4yu4ABLuOgOjeFxX3dokG24ztSRg5tnT00sL8BszO7gSMoIw==
   dependencies:
     "@types/node" "*"
 
 "@types/http-cache-semantics@*":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a"
-  integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
+  integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
 
 "@types/ioredis@*":
-  version "4.26.4"
-  resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.26.4.tgz#a2b1ed51ddd2c707d7eaac5017cc34a0fe51558a"
-  integrity sha512-QFbjNq7EnOGw6d1gZZt2h26OFXjx7z+eqEnbCHSrDI1OOLEgOHMKdtIajJbuCr9uO+X9kQQRe7Lz6uxqxl5XKg==
+  version "4.26.6"
+  resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.26.6.tgz#7e332d6d24f12d79a1099834ccfa0c169ef667ed"
+  integrity sha512-Q9ydXL/5Mot751i7WLCm9OGTj5jlW3XBdkdEW21SkXZ8Y03srbkluFGbM3q8c+vzPW30JOLJ+NsZWHoly0+13A==
   dependencies:
     "@types/node" "*"
 
 "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7":
-  version "7.0.7"
-  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
-  integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
-
-"@types/json5@^0.0.29":
-  version "0.0.29"
-  resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
-  integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
+  version "7.0.8"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818"
+  integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==
 
 "@types/keyv@*":
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7"
-  integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.2.tgz#5d97bb65526c20b6e0845f6b0d2ade4f28604ee5"
+  integrity sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==
   dependencies:
     "@types/node" "*"
 
 "@types/lodash@^4.14.64":
-  version "4.14.170"
-  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
-  integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
+  version "4.14.171"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.171.tgz#f01b3a5fe3499e34b622c362a46a609fdb23573b"
+  integrity sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==
 
 "@types/lru-cache@^5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03"
-  integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef"
+  integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==
 
 "@types/magnet-uri@*", "@types/magnet-uri@^5.1.1":
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/@types/magnet-uri/-/magnet-uri-5.1.2.tgz#7860417399d52ddc0be1021d570b4ac93ffc133e"
-  integrity sha512-bXFPXskwHoEYP6t8rq4nWchOlbUzXkyhnfCVZmq+zb25R5pWkasw7BmTIqDKQ6RAQmq89jll1v23yLa/SvPfAw==
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/@types/magnet-uri/-/magnet-uri-5.1.3.tgz#cdf974721012bd758c0f559cabcad7bab87f9008"
+  integrity sha512-FvJN1yYdLhvU6zWJ2YnWQ2GnpFLsA8bt+85WY0tLh6ehzGNrvBorjlcc53/zY43r/IKn+ctFs1nt7andwGnQCQ==
   dependencies:
     "@types/node" "*"
 
-"@types/maildev@^0.0.2":
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/@types/maildev/-/maildev-0.0.2.tgz#936f21d66d2c38fafdd653d5bee8b642eb1dab89"
-  integrity sha512-ITMKrdajIgqe5lz0BrU2xFB3yN4gX/+a2vemuPfgURlcxLn7V5i9AuzGQl2wiH2cg7zcZBqh8EHX+z3ufF7AUA==
+"@types/maildev@^0.0.3":
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/@types/maildev/-/maildev-0.0.3.tgz#8a7e3cc640d5388d86bcd11f6c18e40926244b87"
+  integrity sha512-fY5WoW3zsW686UFKf5ISIWiolaYoo+kbFE/B1rOHNJ768gdyIgu3Wol02vwanq2gFQO420iTUDaZ7ZpQbjZ57Q==
   dependencies:
     "@types/node" "*"
 
 "@types/memoizee@^0.4.2":
-  version "0.4.5"
-  resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.5.tgz#cb4e7031decf698c52c4f57c348180b0385aa7da"
-  integrity sha512-+ZzZZ3+0a7/ajBPeAAD4+LxrBsCat0EFZQtO3o0rwpIeLmDmSaM8KF/oYPuFxeUFAMiHIHFcGucFnY/8S4Hszg==
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.6.tgz#a4ba7a3ea61fa45be916f148763bec2ca38c34cf"
+  integrity sha512-qJezGqoi3pW9Pset2w1Gfv8jATvmHHHnpO9Dq8x8pJGyYIpiUZJqRU0NM7xenmN0AcXEe7vqshI8H98KeFLYcg==
 
 "@types/mime@^1":
   version "1.3.2"
   integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
 
 "@types/minimatch@^3.0.3":
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
-  integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
+  integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
 
 "@types/mkdirp@^1.0.0":
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-1.0.1.tgz#0930b948914a78587de35458b86c907b6e98bbf6"
-  integrity sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-1.0.2.tgz#8d0bad7aa793abe551860be1f7ae7f3198c16666"
+  integrity sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==
   dependencies:
     "@types/node" "*"
 
 "@types/mocha@^8.0.3":
-  version "8.2.2"
-  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.2.tgz#91daa226eb8c2ff261e6a8cbf8c7304641e095e0"
-  integrity sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw==
+  version "8.2.3"
+  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
+  integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
 
 "@types/morgan@^1.7.32":
-  version "1.9.2"
-  resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.2.tgz#450f958a4d3fb0694a3ba012b09c8106f9a2885e"
-  integrity sha512-edtGMEdit146JwwIeyQeHHg9yID4WSolQPxpEorHmN3KuytuCHyn2ELNr5Uxy8SerniFbbkmgKMrGM933am5BQ==
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.3.tgz#ae04180dff02c437312bc0cfb1e2960086b2f540"
+  integrity sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q==
   dependencies:
     "@types/node" "*"
 
 "@types/multer@^1.3.3":
-  version "1.4.6"
-  resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.6.tgz#411950b7a99ba0de6ee8f6e3713f4628980cdc73"
-  integrity sha512-F4EZ+KRrzdiSm3jSFj1GVUlw3zWXus5nXYBbrQW/0MGIUv9YHw1dM0cJOxq++v2+Gl4IBdSDvQ3YCORLdyCU+Q==
+  version "1.4.7"
+  resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e"
+  integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==
   dependencies:
     "@types/express" "*"
 
-"@types/node@*", "@types/node@>=10.0.0", "@types/node@^15.0.1":
-  version "15.12.4"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26"
-  integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==
+"@types/node@*", "@types/node@>=10.0.0":
+  version "16.4.0"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.0.tgz#2c219eaa3b8d1e4d04f4dd6e40bc68c7467d5272"
+  integrity sha512-HrJuE7Mlqcjj+00JqMWpZ3tY8w7EUd+S0U3L1+PQSWiXZbOgyQDvi+ogoUxaHApPJq5diKxYBQwA3iIlNcPqOg==
 
 "@types/node@^14.14.31":
-  version "14.17.4"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.4.tgz#218712242446fc868d0e007af29a4408c7765bc0"
-  integrity sha512-8kQ3+wKGRNN0ghtEn7EGps/B8CzuBz1nXZEIGGLP2GnwbqYn4dbTs7k+VKLTq1HvZLRCIDtN3Snx1Ege8B7L5A==
+  version "14.17.5"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.5.tgz#b59daf6a7ffa461b5648456ca59050ba8e40ed54"
+  integrity sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==
+
+"@types/node@^15.0.1":
+  version "15.14.2"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.2.tgz#7af8ab20156586f076f4760bc1b3c5ddfffd1ff2"
+  integrity sha512-dvMUE/m2LbXPwlvVuzCyslTEtQ2ZwuuFClDrOQ6mp2CenCg971719PTILZ4I6bTP27xfFFc+o7x2TkLuun/MPw==
 
 "@types/nodemailer@^6.2.0":
-  version "6.4.2"
-  resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.2.tgz#d8ee254c969e6ad83fb9a0a0df3a817406a3fa3b"
-  integrity sha512-yhsqg5Xbr8aWdwjFS3QjkniW5/tLpWXtOYQcJdo9qE3DolBxsKzgRCQrteaMY0hos8MklJNSEsMqDpZynGzMNg==
+  version "6.4.4"
+  resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
+  integrity sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==
   dependencies:
     "@types/node" "*"
 
 "@types/normalize-package-data@^2.4.0":
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
-  integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
+  integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
 
 "@types/oauth2-server@^3.0.8":
-  version "3.0.12"
-  resolved "https://registry.yarnpkg.com/@types/oauth2-server/-/oauth2-server-3.0.12.tgz#3c404055c2c88068a3ee8f5e5a0c5a12bca8c365"
-  integrity sha512-biRndg8t05UxbW1Aqe9kqDbzoi3wSgKITQxtLKR2eK0SwWTgF6AS32IyriFX6qf8KZWhruViVat7MuFfuAUrZQ==
+  version "3.0.13"
+  resolved "https://registry.yarnpkg.com/@types/oauth2-server/-/oauth2-server-3.0.13.tgz#e93baf99a923ffbb9ef09dea9978ee63d706b96c"
+  integrity sha512-thC6D0vgqUh2LFeOV7AzuWaAjZfciFmnh2tM10Nw4ZllMnTP7jw8PYMY6ti7PHAkwp4Cz9YFZs2LFwlXlA87Bw==
   dependencies:
     "@types/express" "*"
 
 "@types/parse-torrent-file@*":
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/@types/parse-torrent-file/-/parse-torrent-file-4.0.2.tgz#40c96fc075aec256514807c6c381d11d9035bd9e"
-  integrity sha512-EzdzpcN0sStQ35sUV2SChTJErLsbotsxZ/RYeR9gf3zXKlPLKaA7aIAoS/nuLRvfxH8mbrWQmXSw76alKecSdg==
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/@types/parse-torrent-file/-/parse-torrent-file-4.0.3.tgz#045b023426d168e0253c932cb782b231b1ee2d62"
+  integrity sha512-dFkPnJPKiFWiGX+HXmyTVt2js3k0d9dThmUxX8nfGC22hbyZ5BTmetsEl45sQhHLcFo43njVrIKMXM3F1ahXRw==
   dependencies:
     "@types/node" "*"
 
 "@types/parse-torrent@*":
-  version "5.8.3"
-  resolved "https://registry.yarnpkg.com/@types/parse-torrent/-/parse-torrent-5.8.3.tgz#ff4e987d09ad27ccc1c8893b3a2c6a31a3bc4042"
-  integrity sha512-c0xAjnpov+Xk/2HTtpaBm0tukNIAoZoxrqgTDwSaIu6IVCynY+2YD9zcQNk2P6H4atcXzD78/LI2CQzLlMmAJg==
+  version "5.8.4"
+  resolved "https://registry.yarnpkg.com/@types/parse-torrent/-/parse-torrent-5.8.4.tgz#c095834a9a815507c59014a79517ad403e4329d0"
+  integrity sha512-FdKs5yN5iYO5Cu9gVz1Zl30CbZe6HTsqloWmCf+LfbImgSzlsUkov2+npQWCQSQ3zi/a2G5C824K0UpZ2sRufA==
   dependencies:
     "@types/magnet-uri" "*"
     "@types/node" "*"
     "@types/parse-torrent-file" "*"
 
 "@types/pem@^1.9.3":
-  version "1.9.5"
-  resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.5.tgz#cd5548b5e0acb4b41a9e21067e9fcd8c57089c99"
-  integrity sha512-C0txxEw8B7DCoD85Ko7SEvzUogNd5VDJ5/YBG8XUcacsOGqxr5Oo4g3OUAfdEDUbhXanwUoVh/ZkMFw77FGPQQ==
+  version "1.9.6"
+  resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.6.tgz#c3686832e935947fdd9d848dec3b8fe830068de7"
+  integrity sha512-IC67SxacM9fxEi/w7hf98dTun83OwUMeLMo1NS2gE0wdM9MHeg73iH/Pp9nB02OUCQ7Zb2UuKE/IpFCmQw9jxw==
   dependencies:
     "@types/node" "*"
 
 "@types/qs@*":
-  version "6.9.6"
-  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1"
-  integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==
+  version "6.9.7"
+  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
+  integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
 
 "@types/range-parser@*":
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
-  integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
+  integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
 
-"@types/redis@*", "@types/redis@^2.8.5":
-  version "2.8.30"
-  resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.30.tgz#2b63ce9ff93959355d8a1f6d7a45e483b5fe0299"
-  integrity sha512-4D3XwfIc671FSNXNruE/wmf6jWL7QYtyAhiWXJDkY41F4atMnOol4584oP4WqnW3uHe8d+Jn+wDLuQaxbfMgXQ==
+"@types/redis@^2.8.5":
+  version "2.8.31"
+  resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.31.tgz#c11c1b269fec132ac2ec9eb891edf72fc549149e"
+  integrity sha512-daWrrTDYaa5iSDFbgzZ9gOOzyp2AJmYK59OlG/2KGBgYWF3lfs8GDKm1c//tik5Uc93hDD36O+qLPvzDolChbA==
   dependencies:
     "@types/node" "*"
 
 "@types/request@^2.0.3":
-  version "2.48.5"
-  resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0"
-  integrity sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==
+  version "2.48.6"
+  resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.6.tgz#2300e7fc443108f79efa90e3bdf34c6d60fa89d8"
+  integrity sha512-vrZaV3Ij7j/l/3hz6OttZFtpRCu7zlq7XgkYHJP6FwVEAZkGQ095WqyJV08/GlW9eyXKVcp/xmtruHm8eHpw1g==
   dependencies:
     "@types/caseless" "*"
     "@types/node" "*"
     "@types/node" "*"
 
 "@types/sax@^1.2.1":
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.1.tgz#e0248be936ece791a82db1a57f3fb5f7c87e8172"
-  integrity sha512-dqYdvN7Sbw8QT/0Ci5rhjE4/iCMJEM0Y9rHpCu+gGXD9Lwbz28t6HI2yegsB6BoV1sShRMU6lAmAcgRjmFy7LA==
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.3.tgz#b630ac1403ebd7812e0bf9a10de9bf5077afb348"
+  integrity sha512-+QSw6Tqvs/KQpZX8DvIl3hZSjNFLW/OqE5nlyHXtTwODaJvioN2rOWpBNEWZp2HZUFhOh+VohmJku/WxEXU2XA==
   dependencies:
     "@types/node" "*"
 
 "@types/serve-static@*":
-  version "1.13.9"
-  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e"
-  integrity sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==
+  version "1.13.10"
+  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
+  integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==
   dependencies:
     "@types/mime" "^1"
     "@types/node" "*"
 
 "@types/simple-peer@*":
-  version "9.11.0"
-  resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.11.0.tgz#72f369cb2bebd0023b265aa726d352bf744f9f4b"
-  integrity sha512-HjIGo3D5I2tdtl2FpngcDYHgqlDxZuZqmQn7f0TQrpYI4ZbuHbBg9THoiDjzKjCzZsHrG5Ag53m5O/cBYfjQWA==
+  version "9.11.1"
+  resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.11.1.tgz#bef6ff1e75178d83438e33aa6a4df2fd98fded1d"
+  integrity sha512-Pzqbau/WlivSXdRC0He2Wz/ANj2wbi4gzJrtysZz93jvOyI2jo/ibMjUe6AvPllFl/UO6QXT/A0Rcp44bDQB5A==
   dependencies:
     "@types/node" "*"
 
 "@types/superagent@*":
-  version "4.1.11"
-  resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.11.tgz#4822bc64a82a0f579261a77097dbca276556c20e"
-  integrity sha512-cZkWBXZI+jESnUTp8RDGBmk1Zn2MkScP4V5bjD7DyqB7L0WNWpblh4KX5K/6aTqxFZMhfo1bhi2cwoAEDVBBJw==
+  version "4.1.12"
+  resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.12.tgz#fad68c6712936892ad24cf94f2f7a07cc749fd0f"
+  integrity sha512-1GQvD6sySQPD6p9EopDFI3f5OogdICl1sU/2ij3Esobz/RtL9fWZZDPmsuv7eiy5ya+XNiPAxUcI3HIUTJa+3A==
   dependencies:
     "@types/cookiejar" "*"
     "@types/node" "*"
     "@types/superagent" "*"
 
 "@types/tough-cookie@*":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
-  integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.1.tgz#8f80dd965ad81f3e1bc26d6f5c727e132721ff40"
+  integrity sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==
 
 "@types/tv4@*":
-  version "1.2.30"
-  resolved "https://registry.yarnpkg.com/@types/tv4/-/tv4-1.2.30.tgz#3159a6fd5ac90c42d27aecfb20a6a150fb565668"
-  integrity sha512-Uj68uOn6T94IQsEGxLRrFiAqYsP4AUaXiYWrm+DR9/PNtIxuXaq/oHwBbGVR0uXHox4UZMKxCuf+z5M40MpoBw==
+  version "1.2.31"
+  resolved "https://registry.yarnpkg.com/@types/tv4/-/tv4-1.2.31.tgz#b33f3e6f082782a90f1fc5f414ad8722db8c9baa"
+  integrity sha512-P97XU07fcpauSw3/fE2Q7eF6bHl4oHhwkikjnM7zlQLENrdC2rZuHSdNlMBhnW82NyBEsVJHII1Jk3d/MtQsQQ==
 
 "@types/validator@^13.0.0":
-  version "13.1.4"
-  resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.1.4.tgz#d2e3c27523ce1b5d9dc13d16cbce65dc4db2adbe"
-  integrity sha512-19C02B8mr53HufY7S+HO/EHBD7a/R22IwEwyqiHaR19iwL37dN3o0M8RianVInfSSqP7InVSg/o0mUATM4JWsQ==
+  version "13.6.3"
+  resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.6.3.tgz#31ca2e997bf13a0fffca30a25747d5b9f7dbb7de"
+  integrity sha512-fWG42pMJOL4jKsDDZZREnXLjc3UE0R8LOJfARWYg6U966rxDT7TYejYzLnUF5cvSObGg34nd0+H2wHHU5Omdfw==
 
 "@types/webtorrent@^0.109.0":
-  version "0.109.0"
-  resolved "https://registry.yarnpkg.com/@types/webtorrent/-/webtorrent-0.109.0.tgz#dd377691caf360317738f67fa0c3bce48623df57"
-  integrity sha512-c6EgbuFRZqhM4TMnloRuLAcR45j/Qn0kQ6CKWMppXXHfaQpspB1ZeeYx2Bpc22MAgCc3pjlAakTRS2h15stW2A==
+  version "0.109.1"
+  resolved "https://registry.yarnpkg.com/@types/webtorrent/-/webtorrent-0.109.1.tgz#6ca843c3a6d442459b558ec25ce290437f204900"
+  integrity sha512-yH0F7Ma9VI7Y1y02ZIOkDlS9WDoTFwesUGUFxjDEE6OFLlSqIKxgdHY72cigr7JHCwDm6uNQnCq+twz0SO6cTw==
   dependencies:
     "@types/bittorrent-protocol" "*"
     "@types/node" "*"
     "@types/simple-peer" "*"
 
 "@types/ws@^7.2.1":
-  version "7.4.5"
-  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.5.tgz#8ff0f7efcd8fea19f51f9dd66cb8b498d172a752"
-  integrity sha512-8mbDgtc8xpxDDem5Gwj76stBDJX35KQ3YBoayxlqUQcL5BZUthiqP/VQ4PQnLHqM4PmlbyO74t98eJpURO+gPA==
+  version "7.4.7"
+  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
+  integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==
   dependencies:
     "@types/node" "*"
 
 "@typescript-eslint/eslint-plugin@^4.8.1":
-  version "4.28.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz#1a66f03b264844387beb7dc85e1f1d403bd1803f"
-  integrity sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ==
+  version "4.28.4"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.4.tgz#e73c8cabbf3f08dee0e1bda65ed4e622ae8f8921"
+  integrity sha512-s1oY4RmYDlWMlcV0kKPBaADn46JirZzvvH7c2CtAqxCY96S538JRBAzt83RrfkDheV/+G/vWNK0zek+8TB3Gmw==
   dependencies:
-    "@typescript-eslint/experimental-utils" "4.28.0"
-    "@typescript-eslint/scope-manager" "4.28.0"
+    "@typescript-eslint/experimental-utils" "4.28.4"
+    "@typescript-eslint/scope-manager" "4.28.4"
     debug "^4.3.1"
     functional-red-black-tree "^1.0.1"
     regexpp "^3.1.0"
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/experimental-utils@4.28.0":
-  version "4.28.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz#13167ed991320684bdc23588135ae62115b30ee0"
-  integrity sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ==
+"@typescript-eslint/experimental-utils@4.28.4":
+  version "4.28.4"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.4.tgz#9c70c35ebed087a5c70fb0ecd90979547b7fec96"
+  integrity sha512-OglKWOQRWTCoqMSy6pm/kpinEIgdcXYceIcH3EKWUl4S8xhFtN34GQRaAvTIZB9DD94rW7d/U7tUg3SYeDFNHA==
   dependencies:
     "@types/json-schema" "^7.0.7"
-    "@typescript-eslint/scope-manager" "4.28.0"
-    "@typescript-eslint/types" "4.28.0"
-    "@typescript-eslint/typescript-estree" "4.28.0"
+    "@typescript-eslint/scope-manager" "4.28.4"
+    "@typescript-eslint/types" "4.28.4"
+    "@typescript-eslint/typescript-estree" "4.28.4"
     eslint-scope "^5.1.1"
     eslint-utils "^3.0.0"
 
 "@typescript-eslint/parser@^4.0.0":
-  version "4.28.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.0.tgz#2404c16751a28616ef3abab77c8e51d680a12caa"
-  integrity sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==
+  version "4.28.4"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.4.tgz#bc462dc2779afeefdcf49082516afdc3e7b96fab"
+  integrity sha512-4i0jq3C6n+og7/uCHiE6q5ssw87zVdpUj1k6VlVYMonE3ILdFApEzTWgppSRG4kVNB/5jxnH+gTeKLMNfUelQA==
   dependencies:
-    "@typescript-eslint/scope-manager" "4.28.0"
-    "@typescript-eslint/types" "4.28.0"
-    "@typescript-eslint/typescript-estree" "4.28.0"
+    "@typescript-eslint/scope-manager" "4.28.4"
+    "@typescript-eslint/types" "4.28.4"
+    "@typescript-eslint/typescript-estree" "4.28.4"
     debug "^4.3.1"
 
-"@typescript-eslint/scope-manager@4.28.0":
-  version "4.28.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz#6a3009d2ab64a30fc8a1e257a1a320067f36a0ce"
-  integrity sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==
+"@typescript-eslint/scope-manager@4.28.4":
+  version "4.28.4"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.4.tgz#bdbce9b6a644e34f767bd68bc17bb14353b9fe7f"
+  integrity sha512-ZJBNs4usViOmlyFMt9X9l+X0WAFcDH7EdSArGqpldXu7aeZxDAuAzHiMAeI+JpSefY2INHrXeqnha39FVqXb8w==
   dependencies:
-    "@typescript-eslint/types" "4.28.0"
-    "@typescript-eslint/visitor-keys" "4.28.0"
+    "@typescript-eslint/types" "4.28.4"
+    "@typescript-eslint/visitor-keys" "4.28.4"
 
-"@typescript-eslint/types@4.28.0":
-  version "4.28.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.0.tgz#a33504e1ce7ac51fc39035f5fe6f15079d4dafb0"
-  integrity sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==
+"@typescript-eslint/types@4.28.4":
+  version "4.28.4"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.4.tgz#41acbd79b5816b7c0dd7530a43d97d020d3aeb42"
+  integrity sha512-3eap4QWxGqkYuEmVebUGULMskR6Cuoc/Wii0oSOddleP4EGx1tjLnZQ0ZP33YRoMDCs5O3j56RBV4g14T4jvww==
 
-"@typescript-eslint/typescript-estree@4.28.0":
-  version "4.28.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz#e66d4e5aa2ede66fec8af434898fe61af10c71cf"
-  integrity sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==
+"@typescript-eslint/typescript-estree@4.28.4":
+  version "4.28.4"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.4.tgz#252e6863278dc0727244be9e371eb35241c46d00"
+  integrity sha512-z7d8HK8XvCRyN2SNp+OXC2iZaF+O2BTquGhEYLKLx5k6p0r05ureUtgEfo5f6anLkhCxdHtCf6rPM1p4efHYDQ==
   dependencies:
-    "@typescript-eslint/types" "4.28.0"
-    "@typescript-eslint/visitor-keys" "4.28.0"
+    "@typescript-eslint/types" "4.28.4"
+    "@typescript-eslint/visitor-keys" "4.28.4"
     debug "^4.3.1"
     globby "^11.0.3"
     is-glob "^4.0.1"
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/visitor-keys@4.28.0":
-  version "4.28.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz#255c67c966ec294104169a6939d96f91c8a89434"
-  integrity sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==
+"@typescript-eslint/visitor-keys@4.28.4":
+  version "4.28.4"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.4.tgz#92dacfefccd6751cbb0a964f06683bfd72d0c4d3"
+  integrity sha512-NIAXAdbz1XdOuzqkJHjNKXKj8QQ4cv5cxR/g0uQhCYf/6//XrmfpaYsM7PnBcNbfvTDLUkqQ5TPNm1sozDdTWg==
   dependencies:
-    "@typescript-eslint/types" "4.28.0"
+    "@typescript-eslint/types" "4.28.4"
     eslint-visitor-keys "^2.0.0"
 
 "@ungap/promise-all-settled@1.1.2":
   integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
 
 "@uploadx/core@^4.4.0":
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/@uploadx/core/-/core-4.4.0.tgz#27ea2b0d28125e81a6bdd65637dc5c7829306cc7"
-  integrity sha512-dU0oDURYR5RvuAzf63EL9e/fCY4OOQKOs237UTbZDulbRbiyxwEZR+IpRYYr3hKRjjij03EF/Y5j54VGkebAKg==
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/@uploadx/core/-/core-4.4.2.tgz#13220a449e3ab23ed324e4beaea04dd56e538b10"
+  integrity sha512-wI9iWjuT+FDV/IjTj55xPs30MwiIMg4buoZlBnTiPAkgyxpwXi9F+6Zchjy2oE5Lfd/9enp0sQz/0RMfqVimAg==
   dependencies:
     bytes "^3.1.0"
     debug "^4.3.1"
@@ -1121,9 +1128,9 @@ accepts@~1.3.4, accepts@~1.3.7:
     negotiator "0.6.2"
 
 acorn-jsx@^5.3.1:
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
-  integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
+  version "5.3.2"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+  integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
 
 acorn@^7.1.1, acorn@^7.4.0:
   version "7.4.1"
@@ -1163,9 +1170,9 @@ ajv@^6.10.0, ajv@^6.12.4:
     uri-js "^4.2.2"
 
 ajv@^8.0.1:
-  version "8.6.0"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.0.tgz#60cc45d9c46a477d80d92c48076d972c342e5720"
-  integrity sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ==
+  version "8.6.2"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571"
+  integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==
   dependencies:
     fast-deep-equal "^3.1.1"
     json-schema-traverse "^1.0.0"
@@ -1235,7 +1242,7 @@ any-promise@^1.3.0:
   resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
   integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
 
-anymatch@~3.1.1, anymatch@~3.1.2:
+anymatch@~3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
   integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
@@ -1243,11 +1250,6 @@ anymatch@~3.1.1, anymatch@~3.1.2:
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
 
-apicache@1.6.2:
-  version "1.6.2"
-  resolved "https://registry.yarnpkg.com/apicache/-/apicache-1.6.2.tgz#a0a3d51024fa2814c4ace7e9e7053ebcb0920ee6"
-  integrity sha512-3z5e+1E2qwZoqzFVgdx5l9nGhSG0kHv3v2G170vnJSz5uj4mCLVZfRw0o37aWwV8pTPXSkB8OBZz3TIur4H26g==
-
 append-field@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
@@ -1408,9 +1410,9 @@ asynckit@^0.4.0:
   integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
 
 autocannon@^7.0.4:
-  version "7.3.0"
-  resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.3.0.tgz#8e52bb3f07926b573dcf401d3fe365393fbab808"
-  integrity sha512-RuyTU8fQE1FC6BDgslofLCeI4y9y9RRnnZtvoQ+onwQl+B2rsiGpcCi84/Si0rq3VRkkMFnmPulY3z59zYhX9g==
+  version "7.4.0"
+  resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.4.0.tgz#7e3ea188501d60162b7a0167c1d9bf90db870c2e"
+  integrity sha512-X0g/nkJ7oHkfn/B+LUzz9jPjG4nD48tPnu2E24m7LA7p8+Mvd/Clrb+FnHmPgI7pszXPRtzUYWz6QrSyMiEY6w==
   dependencies:
     chalk "^4.1.0"
     char-spinner "^1.0.1"
@@ -1575,10 +1577,10 @@ bitfield@^4.0.0:
   resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-4.0.0.tgz#3094123c870030dc6198a283d779639bd2a8e256"
   integrity sha512-jtuSG9CQr5yoHFuvhgf50+DH8Aezl3C/mMSfqdG4DqP7Kqe34uBUtCEHPN9oWaldTm96/i7y5e778SnM5ES4rw==
 
-bittorrent-dht@^10.0.0:
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/bittorrent-dht/-/bittorrent-dht-10.0.0.tgz#01de59bb03ed86a8847cb533134925d236d7f565"
-  integrity sha512-mrM18HMabvd3n/hQa4PYe942nWvBsJCBQb5PfT9kUJLlspNPGiulZYSCgWs7+XarS7nufYrGEp07f9eKTKIrgw==
+bittorrent-dht@^10.0.0, bittorrent-dht@^10.0.1:
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/bittorrent-dht/-/bittorrent-dht-10.0.1.tgz#ff2efe77cdb4d72c819f46b42a162f42ca233793"
+  integrity sha512-aR0vAgm+SgLiwTCEtNgeuqtT2deg+E/xHCTb7iryikvLbqbR58oFHbNYX4CM6EzyNGSKfcdBKp1gWI5Gcn2Aaw==
   dependencies:
     bencode "^2.0.0"
     debug "^4.1.1"
@@ -1603,14 +1605,13 @@ bittorrent-peerid@^1.3.3:
   resolved "https://registry.yarnpkg.com/bittorrent-peerid/-/bittorrent-peerid-1.3.3.tgz#b8dc79e421f8136d2ffd0b163a18e9d70da09949"
   integrity sha512-tSh9HdQgwyEAfo1jzoGEis6o/zs4CcdRTchG93XVl5jct+DCAN90M5MVUV76k2vJ9Xg3GAzLB5NLsY/vnVTh6w==
 
-bittorrent-protocol@^3.3.1:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/bittorrent-protocol/-/bittorrent-protocol-3.4.1.tgz#b481d09dbf910fa7fcca5f06a7c1c4246151d4d1"
-  integrity sha512-3qBW4ZZrUZKN7HzHbX4+kbiphrTNeraMp3i9n3wobicysjibAV8SBDY+sGiBN4SgXV6WvEW4kyRPIjoSqW+khw==
+bittorrent-protocol@^3.4.2:
+  version "3.4.2"
+  resolved "https://registry.yarnpkg.com/bittorrent-protocol/-/bittorrent-protocol-3.4.2.tgz#e5194d1acd30273ac02bb272c208977716d0394b"
+  integrity sha512-a7ueJzmCImWIXfKrJ+dT6mgqi5+LFByAXoMXhV/cYt/y8kplaC8N9ZWfpiTidJY4H2o1GTsyMy73o62a/rZ0Ow==
   dependencies:
     bencode "^2.0.1"
     bitfield "^4.0.0"
-    buffer-xor "^2.0.2"
     debug "^4.3.1"
     randombytes "^2.1.0"
     rc4 "^0.1.5"
@@ -1620,9 +1621,9 @@ bittorrent-protocol@^3.3.1:
     unordered-array-remove "^1.0.2"
 
 bittorrent-tracker@^9.0.0:
-  version "9.17.2"
-  resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.17.2.tgz#1afb02d3d2fb474c13389c45e8a2b6919bff40bd"
-  integrity sha512-hXjed0OnB16da+ScJUZnrAZbf9gMgSLKqh5rJebtYnTRgN4o1mX0DOPH3Nf5RFCs935ibhSmZN5nwbkh+3MdEA==
+  version "9.17.4"
+  resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.17.4.tgz#663f51064a924e945cb6ca19a0c293aca258128b"
+  integrity sha512-ykhdVQHtLfn4DYSJUQD/zFAbP8YwnF6nGlj2SBnCY4xkW5bhwXPeFZUhryAtdITl0qNL/FpmFOamBZfxIwkbxg==
   dependencies:
     bencode "^2.0.1"
     bittorrent-peerid "^1.3.3"
@@ -1793,13 +1794,6 @@ buffer-writer@2.0.0:
   resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
   integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==
 
-buffer-xor@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-2.0.2.tgz#34f7c64f04c777a1f8aac5e661273bb9dd320289"
-  integrity sha512-eHslX0bin3GB+Lx2p7lEYRShRewuNZL3fUl4qlVJGGiwoPGftmt8JQgk2Y9Ji5/01TnVDo33E5b5O3vUB1HdqQ==
-  dependencies:
-    safe-buffer "^5.1.1"
-
 buffer@^5.2.0, buffer@^5.5.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
@@ -1824,9 +1818,9 @@ bufferutil@^4.0.3:
     node-gyp-build "^4.2.0"
 
 bull@^3.4.2:
-  version "3.22.9"
-  resolved "https://registry.yarnpkg.com/bull/-/bull-3.22.9.tgz#9d86493e1bb4afeb7e6e259c45141185bd78111d"
-  integrity sha512-waVaXkjS1+aPxZpmqcKKQX1KeWx7uZXcg9bhe+y5xx/3k8Xu0vqUL1FxMUfp0f3O4KSAHk1EDlD5DGXxBlFDFQ==
+  version "3.26.0"
+  resolved "https://registry.yarnpkg.com/bull/-/bull-3.26.0.tgz#c6198cf4f3a2fa5f3044cbe462b452c77a3df94f"
+  integrity sha512-W1ohwMBApLW9dhKHEwgzr8YnpScTOGC9KtKP2DrvjnWTQFWbaEnKlrDHKp3SJwvAB0C3jDsO579O/Hys/UmAiQ==
   dependencies:
     cron-parser "^2.13.0"
     debuglog "^1.0.0"
@@ -1852,6 +1846,14 @@ bytes@3.1.0, bytes@^3.0.0, bytes@^3.1.0:
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 
+cache-chunk-store@^3.2.2:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/cache-chunk-store/-/cache-chunk-store-3.2.2.tgz#19bb55d61252cd2174da4686548d52bc2dd44120"
+  integrity sha512-2lJdWbgHFFxcSth9s2wpId3CR3v1YC63KjP4T9WhpW7LWlY7Hiiei3QwwqzkWqlJTfR8lSy9F5kRQECeyj+yQA==
+  dependencies:
+    lru "^3.1.0"
+    queue-microtask "^1.2.3"
+
 cacheable-lookup@^5.0.3:
   version "5.0.4"
   resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005"
@@ -2043,22 +2045,7 @@ cheerio@^1.0.0-rc.3:
     parse5-htmlparser2-tree-adapter "^6.0.1"
     tslib "^2.2.0"
 
-chokidar@3.5.1:
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
-  integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==
-  dependencies:
-    anymatch "~3.1.1"
-    braces "~3.0.2"
-    glob-parent "~5.1.0"
-    is-binary-path "~2.1.0"
-    is-glob "~4.0.1"
-    normalize-path "~3.0.0"
-    readdirp "~3.5.0"
-  optionalDependencies:
-    fsevents "~2.3.1"
-
-chokidar@^3.2.2, chokidar@^3.4.2:
+chokidar@3.5.2, chokidar@^3.2.2, chokidar@^3.4.2:
   version "3.5.2"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
   integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
@@ -2235,9 +2222,9 @@ color-name@^1.0.0, color-name@~1.1.4:
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
 color-string@^1.5.2:
-  version "1.5.5"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
-  integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
+  integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==
   dependencies:
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
@@ -2607,9 +2594,9 @@ dateformat@^3.0.3:
   integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
 
 dayjs@^1.10.4:
-  version "1.10.5"
-  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.5.tgz#5600df4548fc2453b3f163ebb2abbe965ccfb986"
-  integrity sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g==
+  version "1.10.6"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.6.tgz#288b2aa82f2d8418a6c9d4df5898c0737ad02a63"
+  integrity sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==
 
 debug@2.6.9, debug@^2.2.0, debug@^2.6.9:
   version "2.6.9"
@@ -2618,7 +2605,14 @@ debug@2.6.9, debug@^2.2.0, debug@^2.6.9:
   dependencies:
     ms "2.0.0"
 
-debug@4, debug@4.3.1, debug@^4.0.0, debug@^4.0.1, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@~4.3.1:
+debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@~4.3.1:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
+  dependencies:
+    ms "2.1.2"
+
+debug@4.3.1:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
   integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
@@ -2978,7 +2972,7 @@ engine.io-client@~3.3.1:
     xmlhttprequest-ssl "~1.5.4"
     yeast "0.1.2"
 
-engine.io-client@~5.1.1:
+engine.io-client@~5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-5.1.2.tgz#27108da9b39ae03262443d945caf2caa3655c4cb"
   integrity sha512-blRrgXIE0A/eurWXRzvfCLG7uUFJqfTGFsyJzXSK71srMMGJ2VraBLg8Mdw28uUxSpVicepBN9X7asqpD1mZcQ==
@@ -3023,7 +3017,7 @@ engine.io@~3.3.1:
     engine.io-parser "~2.1.0"
     ws "~6.1.0"
 
-engine.io@~5.1.0:
+engine.io@~5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-5.1.1.tgz#a1f97e51ddf10cbd4db8b5ff4b165aad3760cdd3"
   integrity sha512-aMWot7H5aC8L4/T8qMYbLdvKlZOdJTH54FxfdFunTGvhMx1BHkJOntWArsVfgAZVwAO9LC2sryPWRcEeUzCe5w==
@@ -3280,12 +3274,13 @@ eslint-visitor-keys@^2.0.0:
   integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
 
 eslint@^7.2.0:
-  version "7.29.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0"
-  integrity sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==
+  version "7.31.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca"
+  integrity sha512-vafgJpSh2ia8tnTkNUkwxGmnumgckLh5aAbLa1xRmIn9+owi8qBNGKL+B881kNKNTy7FFqTEkpNkUvmw0n6PkA==
   dependencies:
     "@babel/code-frame" "7.12.11"
-    "@eslint/eslintrc" "^0.4.2"
+    "@eslint/eslintrc" "^0.4.3"
+    "@humanwhocodes/config-array" "^0.5.0"
     ajv "^6.10.0"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
@@ -3424,9 +3419,9 @@ exif-parser@^0.1.12:
   integrity sha1-WKnS1ywCwfbwKg70qRZicrd2CSI=
 
 express-rate-limit@^5.0.0:
-  version "5.2.6"
-  resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.6.tgz#b454e1be8a252081bda58460e0a25bf43ee0f7b0"
-  integrity sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA==
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.3.0.tgz#e7b9d3c2e09ece6e0406a869b2ce00d03fe48aea"
+  integrity sha512-qJhfEgCnmteSeZAeuOKQ2WEIFTX5ajrzE0xS6gCOBCoRQcU+xEzQmgYQQTpzCcqUAAzTEtu4YEih4pnLfvNtew==
 
 express-validator@^6.4.0:
   version "6.12.0"
@@ -3514,16 +3509,15 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
 fast-glob@^3.1.1:
-  version "3.2.5"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661"
-  integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
+  integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==
   dependencies:
     "@nodelib/fs.stat" "^2.0.2"
     "@nodelib/fs.walk" "^1.2.3"
-    glob-parent "^5.1.0"
+    glob-parent "^5.1.2"
     merge2 "^1.3.0"
-    micromatch "^4.0.2"
-    picomatch "^2.2.1"
+    micromatch "^4.0.4"
 
 fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
@@ -3535,20 +3529,25 @@ fast-levenshtein@^2.0.6:
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
 
-fast-safe-stringify@2.0.7, fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7:
+fast-safe-stringify@2.0.7:
   version "2.0.7"
   resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
   integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
 
+fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7:
+  version "2.0.8"
+  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f"
+  integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag==
+
 fast-xml-parser@^3.19.0:
   version "3.19.0"
   resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01"
   integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==
 
 fastq@^1.6.0:
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858"
-  integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.1.tgz#5d8175aae17db61947f8b162cfc7f63264d22807"
+  integrity sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==
   dependencies:
     reusify "^1.0.4"
 
@@ -3653,9 +3652,9 @@ flat@^5.0.0, flat@^5.0.2:
   integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
 
 flatted@^3.1.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
-  integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.1.tgz#bbef080d95fca6709362c73044a1634f7c6e7d05"
+  integrity sha512-OMQjaErSFHmHqZe+PSidH5n8j3O0F2DdnVh8JB4j4eUQ2k6KvB0qGfrKIhapvez5JerBbmWkaLYUYWISaESoXg==
 
 fluent-ffmpeg@^2.1.0:
   version "2.1.2"
@@ -3762,7 +3761,7 @@ fs.realpath@^1.0.0:
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
-fsevents@~2.3.1, fsevents@~2.3.2:
+fsevents@~2.3.2:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
   integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@@ -3866,7 +3865,7 @@ gifwrap@^0.9.2:
     image-q "^1.1.1"
     omggif "^1.0.10"
 
-glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2:
+glob-parent@^5.1.2, glob-parent@~5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -3913,9 +3912,9 @@ global@~4.4.0:
     process "^0.11.10"
 
 globals@^13.6.0, globals@^13.9.0:
-  version "13.9.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb"
-  integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==
+  version "13.10.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.10.0.tgz#60ba56c3ac2ca845cfbf4faeca727ad9dd204676"
+  integrity sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==
   dependencies:
     type-fest "^0.20.2"
 
@@ -4198,11 +4197,11 @@ human-signals@^2.1.0:
   integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
 
 hyperid@^2.0.3:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/hyperid/-/hyperid-2.1.0.tgz#2f5ed7537e87e8fddd344710a610be501b3d2da6"
-  integrity sha512-cSakhxbUsaIuqjfvvcUuvl/Fl342J65xgLLYrYxSSr9qmJ/EJK+S8crS6mIlQd/a7i+Pe4D0MgSrtZPLze+aCw==
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/hyperid/-/hyperid-2.3.1.tgz#70cc2c917b6367c9f7307718be243bc28b258353"
+  integrity sha512-mIbI7Ymn6MCdODaW1/6wdf5lvvXzmPsARN4zTLakMmcziBOuP4PxCBJvHF6kbAIHX6H4vAELx/pDmt0j6Th5RQ==
   dependencies:
-    uuid "^3.4.0"
+    uuid "^8.3.2"
     uuid-parse "^1.1.0"
 
 i18n-locales@^0.0.5:
@@ -4336,10 +4335,10 @@ ini@~1.3.0:
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
   integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
 
-inquirer@8.1.1:
-  version "8.1.1"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.1.1.tgz#7c53d94c6d03011c7bb2a947f0dca3b98246c26a"
-  integrity sha512-hUDjc3vBkh/uk1gPfMAD/7Z188Q8cvTGl0nxwaCdwSbzFh6ZKkZh+s2ozVxbE5G9ZNRyeY0+lgbAIOUFsFf98w==
+inquirer@8.1.2:
+  version "8.1.2"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.1.2.tgz#65b204d2cd7fb63400edd925dfe428bafd422e3d"
+  integrity sha512-DHLKJwLPNgkfwNmsuEUKSejJFbkv0FMO9SMiQbjI3n5NQuCrSIBqP66ggqyz2a6t2qEolKrMjhQ3+W/xXgUQ+Q==
   dependencies:
     ansi-escapes "^4.2.1"
     chalk "^4.1.1"
@@ -4351,7 +4350,7 @@ inquirer@8.1.1:
     mute-stream "0.0.8"
     ora "^5.3.0"
     run-async "^2.4.0"
-    rxjs "^6.6.6"
+    rxjs "^7.2.0"
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
     through "^2.3.6"
@@ -4468,9 +4467,9 @@ is-cidr@^4.0.0:
     cidr-regex "^3.1.1"
 
 is-core-module@^2.2.0, is-core-module@^2.4.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1"
-  integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491"
+  integrity sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg==
   dependencies:
     has "^1.0.3"
 
@@ -4785,14 +4784,7 @@ json-stable-stringify-without-jsonify@^1.0.1:
   resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
   integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
 
-json5@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
-  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
-  dependencies:
-    minimist "^1.2.0"
-
-json5@^2.1.1:
+json5@^2.1.1, json5@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
   integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
@@ -5252,9 +5244,9 @@ markdown-it-emoji@^2.0.0:
   integrity sha512-39j7/9vP/CPCKbEI44oV8yoPJTpvfeReTn/COgRhSpNrjWF3PfP/JUxxB0hxV6ynOY8KH8Y8aX9NMDdo6z+6YQ==
 
 markdown-it@^12.0.4:
-  version "12.0.6"
-  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.6.tgz#adcc8e5fe020af292ccbdf161fe84f1961516138"
-  integrity sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==
+  version "12.1.0"
+  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.1.0.tgz#7ad572caddd336bd27a68d20e86bac1fafe8fb20"
+  integrity sha512-7temG6IFOOxfU0SgzhqR+vr2diuMhyO5uUIEZ3C5NbXhqC9uFUHoU41USYuDFoZRsaY7BEIEei874Z20VMLF6A==
   dependencies:
     argparse "^2.0.1"
     entities "~2.1.0"
@@ -5268,9 +5260,9 @@ marked-man@^0.7.0:
   integrity sha512-zxK5E4jbuARALc+fIUAanM2njVGnrd9YvKrqoDHUg2XwNLJijo39EzMIg59LecHBHsIHNtPqepqnJp4SmL/EVg==
 
 marked@^2.0.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.2.tgz#59579e17b02443312caa1509994d5a0b18ae38e1"
-  integrity sha512-ueJhIvklJJw04qxQbGIAu63EXwwOCYc7yKMBjgagTM4rjC5QtWyqSNgW7jCosV1/Km/1TUfs5qEpAqcGG0Mo5g==
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753"
+  integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==
 
 math-interval-parser@^2.0.1:
   version "2.0.1"
@@ -5385,7 +5377,7 @@ methods@^1.1.2, methods@~1.1.2:
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
   integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
 
-micromatch@^4.0.2:
+micromatch@^4.0.4:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
   integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
@@ -5495,14 +5487,14 @@ mkdirp@^1.0.3, mkdirp@~1.0.4:
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
 
 mocha@^9.0.0:
-  version "9.0.1"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.0.1.tgz#01e66b7af0012330c0a38c4b6eaa6d92b8a81bf9"
-  integrity sha512-9zwsavlRO+5csZu6iRtl3GHImAbhERoDsZwdRkdJ/bE+eVplmoxNKE901ZJ9LdSchYBjSCPbjKc5XvcAri2ylw==
+  version "9.0.2"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.0.2.tgz#e84849b61f406a680ced85af76425f6f3108d1a0"
+  integrity sha512-FpspiWU+UT9Sixx/wKimvnpkeW0mh6ROAKkIaPokj3xZgxeRhcna/k5X57jJghEr8X+Cgu/Vegf8zCX5ugSuTA==
   dependencies:
     "@ungap/promise-all-settled" "1.1.2"
     ansi-colors "4.1.1"
     browser-stdout "1.3.1"
-    chokidar "3.5.1"
+    chokidar "3.5.2"
     debug "4.3.1"
     diff "5.0.0"
     escape-string-regexp "4.0.0"
@@ -5515,12 +5507,12 @@ mocha@^9.0.0:
     minimatch "3.0.4"
     ms "2.1.3"
     nanoid "3.1.23"
-    serialize-javascript "5.0.1"
+    serialize-javascript "6.0.0"
     strip-json-comments "3.1.1"
     supports-color "8.1.1"
     which "2.0.2"
     wide-align "1.1.3"
-    workerpool "6.1.4"
+    workerpool "6.1.5"
     yargs "16.2.0"
     yargs-parser "20.2.4"
     yargs-unparser "2.0.0"
@@ -5711,15 +5703,16 @@ node-gyp-build@^4.2.0:
   integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==
 
 node-media-server@^2.1.4:
-  version "2.2.8"
-  resolved "https://registry.yarnpkg.com/node-media-server/-/node-media-server-2.2.8.tgz#586569690733ff76309f8ff50cdf302f30207009"
-  integrity sha512-sLusJjFVJ3mWeDMHnkyx1gitPjsMcWbhzlXT7wC8gRRjP0HKmY3d8wGLjl0s+JVPB6ruwSZw0mAwntOwrOCnRw==
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/node-media-server/-/node-media-server-2.3.8.tgz#05ad4d1ea9372d4dd5f7b72fb5f1c00da44ce78b"
+  integrity sha512-IWji6X4RQHoiAu0mTojtQ7dznrdsHRahXG51d8um0bjFKkmtJHN3W9oA2fVtFtDrAYMC5bG9GvGDO2qdZtgzww==
   dependencies:
     basic-auth-connect "^1.0.0"
     chalk "^2.4.2"
     dateformat "^3.0.3"
     express "^4.16.4"
     lodash ">=4.17.13"
+    minimist "^1.2.5"
     mkdirp "1.0.3"
     ws "^7.4.6"
 
@@ -5739,14 +5732,14 @@ nodemailer@^3.1.1:
   integrity sha1-/r+sy0vSc2eEc6MJxstLSi88SOM=
 
 nodemailer@^6.0.0, nodemailer@^6.5.0, nodemailer@^6.6.0:
-  version "6.6.2"
-  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.6.2.tgz#e184c9ed5bee245a3e0bcabc7255866385757114"
-  integrity sha512-YSzu7TLbI+bsjCis/TZlAXBoM4y93HhlIgo0P5oiA2ua9Z4k+E2Fod//ybIzdJxOlXGRcHIh/WaeCBehvxZb/Q==
+  version "6.6.3"
+  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.6.3.tgz#31fb53dd4d8ae16fc088a65cb9ffa8d928a69b48"
+  integrity sha512-faZFufgTMrphYoDjvyVpbpJcYzwyFnbAMmQtj1lVBYAUSm3SOy2fIdd9+Mr4UxPosBa0JRw9bJoIwQn+nswiew==
 
 nodemon@^2.0.1:
-  version "2.0.7"
-  resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.7.tgz#6f030a0a0ebe3ea1ba2a38f71bf9bab4841ced32"
-  integrity sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==
+  version "2.0.12"
+  resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.12.tgz#5dae4e162b617b91f1873b3bfea215dd71e144d5"
+  integrity sha512-egCTmNZdObdBxUBw6ZNwvZ/xzk24CKRs5K6d+5zbmrMr7rOpPmfPeF6OxM3DDpaRx331CQRFEktn+wrFFfBSOA==
   dependencies:
     chokidar "^3.2.2"
     debug "^3.2.6"
@@ -5862,9 +5855,9 @@ object-hash@2.1.1:
   integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==
 
 object-inspect@^1.10.3, object-inspect@^1.9.0:
-  version "1.10.3"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
-  integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
+  integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
 
 object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
@@ -6394,9 +6387,9 @@ pngjs@^3.0.0, pngjs@^3.3.3:
   integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
 
 postcss@^8.0.2:
-  version "8.3.5"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709"
-  integrity sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA==
+  version "8.3.6"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea"
+  integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==
   dependencies:
     colorette "^1.2.2"
     nanoid "^3.1.23"
@@ -6847,13 +6840,6 @@ readable-wrap@^1.0.0:
   dependencies:
     readable-stream "^1.1.13-1"
 
-readdirp@~3.5.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e"
-  integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==
-  dependencies:
-    picomatch "^2.2.1"
-
 readdirp@~3.6.0:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -6962,9 +6948,9 @@ require-main-filename@^2.0.0:
   integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
 
 resolve-alpn@^1.0.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.1.2.tgz#30b60cfbb0c0b8dc897940fe13fe255afcdd4d28"
-  integrity sha512-8OyfzhAtA32LVUsJSke3auIyINcwdh5l3cvYKdKO0nvsYSKuiLfTM5i78PJswFPT8y6cPW+L1v6/hE95chcpDA==
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.0.tgz#058bb0888d1cd4d12474e9a4b6eb17bdd5addc44"
+  integrity sha512-e4FNQs+9cINYMO5NMFc6kOUCdohjqFPSgMuwuZAOUWqrfWsen+Yjy5qZFkV5K7VO7tFSLKcUL97olkED7sCBHA==
 
 resolve-from@^4.0.0:
   version "4.0.0"
@@ -7066,14 +7052,14 @@ rusha@^0.8.13:
   resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.14.tgz#a977d0de9428406138b7bb90d3de5dcd024e2f68"
   integrity sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==
 
-rxjs@7.1.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.1.0.tgz#94202d27b19305ef7b1a4f330277b2065df7039e"
-  integrity sha512-gCFO5iHIbRPwznl6hAYuwNFld8W4S2shtSJIqG27ReWXo9IWrCyEICxUA+6vJHwSR/OakoenC4QsDxq50tzYmw==
+rxjs@7.2.0, rxjs@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.2.0.tgz#5cd12409639e9514a71c9f5f9192b2c4ae94de31"
+  integrity sha512-aX8w9OpKrQmiPKfT1bqETtUr9JygIz6GZ+gql8v7CijClsP0laoFUdKzxFAoWuRdSlOdU2+crss+cMf+cqMTnw==
   dependencies:
     tslib "~2.1.0"
 
-rxjs@^6.6.3, rxjs@^6.6.6:
+rxjs@^6.6.3:
   version "6.6.7"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
   integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
@@ -7199,10 +7185,10 @@ sequelize@6.6.2:
     validator "^10.11.0"
     wkx "^0.5.0"
 
-serialize-javascript@5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
-  integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
+serialize-javascript@6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
   dependencies:
     randombytes "^2.1.0"
 
@@ -7385,7 +7371,7 @@ socket.io-adapter@~1.1.0:
   resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9"
   integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==
 
-socket.io-adapter@~2.3.0:
+socket.io-adapter@~2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.1.tgz#a442720cb09a4823cfb81287dda1f9b52d4ccdb2"
   integrity sha512-8cVkRxI8Nt2wadkY6u60Y4rpW3ejA1rxgcK2JuyIhmF+RMNpTy1QRtkHIDUOf3B4HlQwakMsWbKftMv/71VMmw==
@@ -7411,15 +7397,15 @@ socket.io-client@2.2.0:
     to-array "0.1.4"
 
 socket.io-client@^4.0.1:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.1.2.tgz#95ad7113318ea01fba0860237b96d71e1b1fd2eb"
-  integrity sha512-RDpWJP4DQT1XeexmeDyDkm0vrFc0+bUsHDKiVGaNISJvJonhQQOMqV9Vwfg0ZpPJ27LCdan7iqTI92FRSOkFWQ==
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.1.3.tgz#236daa642a9f229932e00b7221e843bf74232a62"
+  integrity sha512-hISFn6PDpgDifVUiNklLHVPTMv1LAk8poHArfIUdXa+gKgbr0MZbAlquDFqCqsF30yBqa+jg42wgos2FK50BHA==
   dependencies:
     "@types/component-emitter" "^1.2.10"
     backo2 "~1.0.2"
     component-emitter "~1.3.0"
     debug "~4.3.1"
-    engine.io-client "~5.1.1"
+    engine.io-client "~5.1.2"
     parseuri "0.0.6"
     socket.io-parser "~4.0.4"
 
@@ -7432,7 +7418,7 @@ socket.io-parser@~3.3.0:
     debug "~3.1.0"
     isarray "2.0.1"
 
-socket.io-parser@~4.0.3, socket.io-parser@~4.0.4:
+socket.io-parser@~4.0.4:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
   integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
@@ -7454,19 +7440,19 @@ socket.io@2.2.0:
     socket.io-parser "~3.3.0"
 
 socket.io@^4.0.1:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.1.2.tgz#f90f9002a8d550efe2aa1d320deebb9a45b83233"
-  integrity sha512-xK0SD1C7hFrh9+bYoYCdVt+ncixkSLKtNLCax5aEy1o3r5PaO5yQhVb97exIe67cE7lAK+EpyMytXWTWmyZY8w==
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.1.3.tgz#d114328ef27ab31b889611792959c3fa6d502500"
+  integrity sha512-tLkaY13RcO4nIRh1K2hT5iuotfTaIQw7cVIe0FUykN3SuQi0cm7ALxuyT5/CtDswOMWUzMGTibxYNx/gU7In+Q==
   dependencies:
     "@types/cookie" "^0.4.0"
-    "@types/cors" "^2.8.8"
+    "@types/cors" "^2.8.10"
     "@types/node" ">=10.0.0"
     accepts "~1.3.4"
     base64id "~2.0.0"
     debug "~4.3.1"
-    engine.io "~5.1.0"
-    socket.io-adapter "~2.3.0"
-    socket.io-parser "~4.0.3"
+    engine.io "~5.1.1"
+    socket.io-adapter "~2.3.1"
+    socket.io-parser "~4.0.4"
 
 source-map-js@^0.6.2:
   version "0.6.2"
@@ -7809,9 +7795,9 @@ superagent@^6.1.0:
     semver "^7.3.2"
 
 supertest@^6.0.1:
-  version "6.1.3"
-  resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.1.3.tgz#3f49ea964514c206c334073e8dc4e70519c7403f"
-  integrity sha512-v2NVRyP73XDewKb65adz+yug1XMtmvij63qIWHZzSX8tp6wiq6xBLUy4SUAd2NII6wIipOmHT/FD9eicpJwdgQ==
+  version "6.1.4"
+  resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.1.4.tgz#ea8953343e0ca316e80e975b39340934f754eb06"
+  integrity sha512-giC9Zm+Bf1CZP09ciPdUyl+XlMAu6rbch79KYiYKOGcbK2R1wH8h+APul1i/3wN6RF1XfWOIF+8X1ga+7SBrug==
   dependencies:
     methods "^1.1.2"
     superagent "^6.1.0"
@@ -8063,10 +8049,10 @@ triple-beam@^1.2.0, triple-beam@^1.3.0:
   resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
   integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
 
-ts-node@10.0.0:
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.0.0.tgz#05f10b9a716b0b624129ad44f0ea05dac84ba3be"
-  integrity sha512-ROWeOIUvfFbPZkoDis0L/55Fk+6gFQNZwwKPLinacRl6tsxstTF1DbAcLKkovwnpKMVvOMHP1TIbnwXwtLg1gg==
+ts-node@10.1.0:
+  version "10.1.0"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.1.0.tgz#e656d8ad3b61106938a867f69c39a8ba6efc966e"
+  integrity sha512-6szn3+J9WyG2hE+5W8e0ruZrzyk1uFLYye6IGMBadnOzDh8aP7t8CbFpsfCiEx2+wMixAhjFt7lOZC4+l+WbEA==
   dependencies:
     "@tsconfig/node10" "^1.0.7"
     "@tsconfig/node12" "^1.0.7"
@@ -8080,12 +8066,11 @@ ts-node@10.0.0:
     yn "3.1.1"
 
 tsconfig-paths@^3.9.0:
-  version "3.9.0"
-  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
-  integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==
+  version "3.10.1"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz#79ae67a68c15289fdf5c51cb74f397522d795ed7"
+  integrity sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q==
   dependencies:
-    "@types/json5" "^0.0.29"
-    json5 "^1.0.1"
+    json5 "^2.2.0"
     minimist "^1.2.0"
     strip-bom "^3.0.0"
 
@@ -8194,9 +8179,9 @@ typedarray@^0.0.6:
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
 typescript@^4.0.5:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc"
-  integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==
+  version "4.3.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
+  integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
 
 uc.micro@^1.0.1, uc.micro@^1.0.5:
   version "1.0.6"
@@ -8322,7 +8307,7 @@ ut_metadata@^3.5.2:
     debug "^4.2.0"
     simple-sha1 "^3.0.1"
 
-ut_pex@^3.0.0:
+ut_pex@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/ut_pex/-/ut_pex-3.0.1.tgz#fb8b6e066f8f6f6de3e6b3e28e7d18e697be5854"
   integrity sha512-t1MHIDHSISgOJcmq8UM6Qv9/hRQYVaUvzqSNnXa5ATDbS9hXfhBpyBo2HcSyJtwPSHsmMtNui8G6yKirwJ8vow==
@@ -8378,10 +8363,10 @@ utils-merge@1.0.1:
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
   integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
 
-utp-native@^2.4.0:
-  version "2.5.1"
-  resolved "https://registry.yarnpkg.com/utp-native/-/utp-native-2.5.1.tgz#445a3fcf23db0a841a48c4e353f8900ea852b3e8"
-  integrity sha512-wbrJwR8DZx8N9s1ffTcMuBK7tMBQ9tvKpIL+mWHrDvGUYfV7ivroEGFTXUr4meqy/PVbUdMfURSoBbwuGtt/YQ==
+utp-native@^2.5.3:
+  version "2.5.3"
+  resolved "https://registry.yarnpkg.com/utp-native/-/utp-native-2.5.3.tgz#7c04c2a8c2858716555a77d10adb9819e3119b25"
+  integrity sha512-sWTrWYXPhhWJh+cS2baPzhaZc89zwlWCfwSthUjGhLkZztyPhcQllo+XVVCbNGi7dhyRlxkWxN4NKU6FbA9Y8w==
   dependencies:
     napi-macros "^2.0.0"
     node-gyp-build "^4.2.0"
@@ -8407,11 +8392,6 @@ uuid@8.3.2, uuid@^8.1.0, uuid@^8.3.0, uuid@^8.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
   integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
 
-uuid@^3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
-  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
-
 v8-compile-cache@^2.0.3:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
@@ -8503,19 +8483,20 @@ webfinger.js@^2.6.6:
     xhr2 "^0.1.4"
 
 webtorrent@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-1.0.2.tgz#acd0ae3bedcfa8cb732043489ff506d07d90140a"
-  integrity sha512-uv9fq5md/98JyeDDyziy1H28Wc/idO80AKv+9pQ4XK0WNNjdE3FMtCKfrvU2VNS1PdAOrA6sFuUq2x0mV7k7WQ==
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-1.2.5.tgz#edea45c53b98787a472381ff91d41c9164b8ac51"
+  integrity sha512-EvtAQ3rK4c7Kf4ZGxYOGvi8Jih8qsZka1IgNB8T5Vxw5UzSNG1nxTVNNTXL0jFhQUMsyRwIOkTgd7ZkJY6bqsw==
   dependencies:
     addr-to-ip-port "^1.5.1"
     bitfield "^4.0.0"
-    bittorrent-dht "^10.0.0"
-    bittorrent-protocol "^3.3.1"
+    bittorrent-dht "^10.0.1"
+    bittorrent-protocol "^3.4.2"
+    cache-chunk-store "^3.2.2"
     chrome-net "^3.3.4"
     chunk-store-stream "^4.3.0"
     cpus "^1.0.3"
     create-torrent "^4.7.0"
-    debug "^4.3.1"
+    debug "^4.3.2"
     end-of-stream "^1.4.4"
     escape-html "^1.0.3"
     fs-chunk-store "^2.0.3"
@@ -8549,9 +8530,9 @@ webtorrent@^1.0.0:
     torrent-piece "^2.0.1"
     unordered-array-remove "^1.0.2"
     ut_metadata "^3.5.2"
-    ut_pex "^3.0.0"
+    ut_pex "^3.0.1"
   optionalDependencies:
-    utp-native "^2.4.0"
+    utp-native "^2.5.3"
 
 which-boxed-primitive@^1.0.2:
   version "1.0.2"
@@ -8659,10 +8640,10 @@ word-wrap@^1.2.3:
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
 
-workerpool@6.1.4:
-  version "6.1.4"
-  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.4.tgz#6a972b6df82e38d50248ee2820aa98e2d0ad3090"
-  integrity sha512-jGWPzsUqzkow8HoAvqaPWTUPCrlPJaJ5tY8Iz7n1uCz3tTp6s3CDG0FF1NsX42WNlkRSW6Mr+CDZGnNoSsKa7g==
+workerpool@6.1.5:
+  version "6.1.5"
+  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.5.tgz#0f7cf076b6215fd7e1da903ff6f22ddd1886b581"
+  integrity sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==
 
 wrap-ansi@^6.2.0:
   version "6.2.0"
@@ -8698,9 +8679,9 @@ write-file-atomic@^3.0.0:
     typedarray-to-buffer "^3.1.5"
 
 ws@^7.0.0, ws@^7.4.2, ws@^7.4.5, ws@^7.4.6:
-  version "7.5.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691"
-  integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw==
+  version "7.5.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
+  integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
 
 ws@~6.1.0:
   version "6.1.4"